From 27793d43ce7cf69a1de399c677c268057930be8a Mon Sep 17 00:00:00 2001 From: iteye Date: Wed, 7 Jan 2026 12:11:44 +0800 Subject: [PATCH 1/4] support repo name --- src/cli/repo/contract.ts | 115 +++++++++++++-------------- src/cli/repo/index.ts | 2 +- src/core/config/abis.ts | 7 +- src/core/config/network.ts | 2 +- src/core/contracts/hub.ts | 17 ++++ src/core/contracts/index.ts | 4 + src/core/contracts/repo.ts | 19 +++++ src/core/contracts/resolver/index.ts | 47 +++++++++++ src/core/contracts/utils.ts | 20 +++++ src/core/index.ts | 1 + 10 files changed, 168 insertions(+), 66 deletions(-) create mode 100644 src/core/contracts/hub.ts create mode 100644 src/core/contracts/index.ts create mode 100644 src/core/contracts/repo.ts create mode 100644 src/core/contracts/resolver/index.ts create mode 100644 src/core/contracts/utils.ts diff --git a/src/cli/repo/contract.ts b/src/cli/repo/contract.ts index c9e8010..a032c3b 100644 --- a/src/cli/repo/contract.ts +++ b/src/cli/repo/contract.ts @@ -1,5 +1,10 @@ -import { ethers, Contract } from "ethers"; -import { GOEFactoryAbi, GOERepoAbi, Networks, WalletManager } from "../../core/index.js" +import { ethers } from "ethers"; +import { + GOEFactoryAbi, + WalletManager, + validateAddress, randomRPC, getNetworkConfig, + resolveRepoAddress, getHubContract, getRepoContract +} from "../../core/index.js" export interface RepoInfo { address: string; @@ -12,17 +17,10 @@ export interface RepoInfo { * Common Helpers * ============================ */ - -function randomRPC(rpcs: string[]): string { - return rpcs[Math.floor(Math.random() * rpcs.length)]; -} - -function getNetworkConfig(chainId: number) { - const config = Networks[chainId]; - if (!config) { - throw new Error(`Unsupported chain ID: ${chainId}.`); - } - return config; +function getOwner(): string { + const address = WalletManager.getDefaultAddress(); + if (!address) throw new Error(`Wallet not found. Please run 'goe wallet create' to create it.`); + return address; } async function getSigner(chainId: number): Promise { @@ -41,18 +39,6 @@ async function waitForTx(tx: ethers.ContractTransactionResponse, action: string) return receipt; } -function validateAddress(address: string, label = "address") { - if (!ethers.isAddress(address)) { - throw new Error(`Invalid ${label}: ${address}`); - } -} - -async function getRepoContract(repoAddress: string, chainId: number) { - validateAddress(repoAddress, "repoAddress"); - const signer = await getSigner(chainId); - return new Contract(repoAddress, GOERepoAbi, signer); -} - /** * ============================ * Factory Contract Methods @@ -61,9 +47,7 @@ async function getRepoContract(repoAddress: string, chainId: number) { export namespace Factory { export async function createRepo(repoName: string, chainId: number): Promise { const signer = await getSigner(chainId); - const { hubAddress } = getNetworkConfig(chainId); - - const factory = new Contract(hubAddress, GOEFactoryAbi, signer); + const factory = await getHubContract(chainId, signer); const tx = await factory.createRepo(ethers.toUtf8Bytes(repoName)); const receipt = await waitForTx(tx, "createRepo"); @@ -79,17 +63,15 @@ export namespace Factory { throw new Error("Transaction succeeded but no 'RepoCreated' event found"); } - export async function getUserReposPaginated( + export async function getReposPaginated( chainId: number, start = 0, limit = 50 ): Promise { - const signer = await getSigner(chainId); - const { hubAddress } = getNetworkConfig(chainId); + const factory = await getHubContract(chainId); + const owner = getOwner(); - const userAddress = await signer.getAddress(); - const factory = new Contract(hubAddress, GOEFactoryAbi, signer); - const repos = await factory.getUserReposPaginated(userAddress, start, limit); + const repos = await factory.getReposPaginated(owner, start, limit); return repos.map((r: any) => ({ address: r.repoAddress, name: ethers.toUtf8String(r.repoName), @@ -103,14 +85,27 @@ export namespace Factory { * Repository Contract Methods * ============================ */ -export namespace Repo { - export async function setDefaultBranch(repoAddress: string, chainId: number, branchName: string) { - const repo = await getRepoContract(repoAddress, chainId); +function normalizeBranchName(branch: string): string { + if (branch.startsWith("refs/")) return branch; + // tags + if (branch.startsWith("tags/")) { + return `refs/${branch}`; + } + + // heads + return `refs/heads/${branch}`; +} + +export namespace Repo { + export async function setDefaultBranch(repoNamespace: string, chainId: number, branchName: string) { + const signer = await getSigner(chainId); + const repoAddress = await resolveRepoAddress(chainId, repoNamespace, getOwner()); + const repo = await getRepoContract(repoAddress, chainId, signer); const fullName = normalizeBranchName(branchName); // check - const branches = await Repo.listBranches(repoAddress, chainId); + const branches = await Repo.listBranches(repoAddress, chainId, repo); const exists = branches.some(b => normalizeBranchName(b.name) === fullName); if (!exists) { throw new Error( @@ -122,41 +117,50 @@ export namespace Repo { await waitForTx(tx, "setDefaultBranch"); } - export async function addPusher(repoAddress: string, chainId: number, account: string) { + export async function addPusher(repoNamespace: string, chainId: number, account: string) { validateAddress(account, "account"); - const repo = await getRepoContract(repoAddress, chainId); + const signer = await getSigner(chainId); + const repoAddress = await resolveRepoAddress(chainId, repoNamespace, getOwner()); + const repo = await getRepoContract(repoAddress, chainId, signer); const tx = await repo.addPusher(account); await waitForTx(tx, "addPusher"); } - export async function removePusher(repoAddress: string, chainId: number, account: string) { + export async function removePusher(repoNamespace: string, chainId: number, account: string) { validateAddress(account, "account"); - const repo = await getRepoContract(repoAddress, chainId); + const signer = await getSigner(chainId); + const repoAddress = await resolveRepoAddress(chainId, repoNamespace, getOwner()); + const repo = await getRepoContract(repoAddress, chainId, signer); const tx = await repo.removePusher(account); await waitForTx(tx, "removePusher"); } - export async function addMaintainer(repoAddress: string, chainId: number, account: string) { + export async function addMaintainer(repoNamespace: string, chainId: number, account: string) { validateAddress(account, "account"); - const repo = await getRepoContract(repoAddress, chainId); + const signer = await getSigner(chainId); + const repoAddress = await resolveRepoAddress(chainId, repoNamespace, getOwner()); + const repo = await getRepoContract(repoAddress, chainId, signer); const tx = await repo.addMaintainer(account); await waitForTx(tx, "addMaintainer"); } - export async function canPush(repoAddress: string, chainId: number, account: string) { + export async function canPush(repoNamespace: string, chainId: number, account: string) { validateAddress(account, "account"); + const repoAddress = await resolveRepoAddress(chainId, repoNamespace, getOwner()); const repo = await getRepoContract(repoAddress, chainId); return repo.canPush(account); } - export async function canForcePush(repoAddress: string, chainId: number, refName: string, account: string) { + export async function canForcePush(repoNamespace: string, chainId: number, refName: string, account: string) { validateAddress(account, "account"); + const repoAddress = await resolveRepoAddress(chainId, repoNamespace, getOwner()); const repo = await getRepoContract(repoAddress, chainId); return repo.canForcePush(account, ethers.toUtf8Bytes(refName)); } - export async function listBranches(repoAddress: string, chainId: number, pageSize: number = 50) { - const repo = await getRepoContract(repoAddress, chainId); + export async function listBranches(repoNamespace: string, chainId: number, repoContract?: ethers.Contract) { + const pageSize = 50; + const repo = repoContract ?? await getRepoContract(await resolveRepoAddress(chainId, repoNamespace, getOwner()), chainId); let start = 0; const all: { name: string; hash: string }[] = []; @@ -185,7 +189,8 @@ export namespace Repo { return all; } - export async function getDefaultBranch(repoAddress: string, chainId: number) { + export async function getDefaultBranch(repoNamespace: string, chainId: number) { + const repoAddress = await resolveRepoAddress(chainId, repoNamespace, getOwner()); const repo = await getRepoContract(repoAddress, chainId); const [refBytes,] = await repo.getDefaultBranch(); if (refBytes.length === 0) { @@ -199,15 +204,3 @@ export namespace Repo { : branch; } } - -function normalizeBranchName(branch: string): string { - if (branch.startsWith("refs/")) return branch; - - // tags - if (branch.startsWith("tags/")) { - return `refs/${branch}`; - } - - // heads - return `refs/heads/${branch}`; -} diff --git a/src/cli/repo/index.ts b/src/cli/repo/index.ts index 0a7b7c0..23b2988 100644 --- a/src/cli/repo/index.ts +++ b/src/cli/repo/index.ts @@ -52,7 +52,7 @@ repoCmd if (chainId === null) return logger.error("Chain ID not specified. Use --chain-id or set GOE_CHAIN_ID environment variable."); try { - const repos = await Factory.getUserReposPaginated(chainId, cmd.start, cmd.limit); + const repos = await Factory.getReposPaginated(chainId, cmd.start, cmd.limit); if (repos.length === 0) return logger.info(`No repositories found on chain ${chainId}.`); logger.success(`Found ${repos.length} repositories:`); diff --git a/src/core/config/abis.ts b/src/core/config/abis.ts index fe27f47..3faa1a9 100644 --- a/src/core/config/abis.ts +++ b/src/core/config/abis.ts @@ -1,9 +1,10 @@ export const GOEFactoryAbi = [ - "event RepoCreated(address indexed repo, address indexed creator, bytes repoName)", + "event RepoCreated(address indexed repo, address indexed owner, bytes repoName)", "function createRepo(bytes repoName) external returns (address)", - "function getUserRepoCount(address user) external view returns (uint256)", - "function getUserReposPaginated(address user, uint256 start, uint256 limit) external view returns (tuple(address repoAddress, uint256 creationTime, bytes repoName)[])" + "function getRepoCount(address user) external view returns (uint256)", + "function getReposPaginated(address user, uint256 start, uint256 limit) external view returns (tuple(address repoAddress, uint256 creationTime, bytes repoName)[])", + "function getRepoByName(address owner, bytes calldata repoName) external view returns (address)", ]; diff --git a/src/core/config/network.ts b/src/core/config/network.ts index 7eb56fb..d72a548 100644 --- a/src/core/config/network.ts +++ b/src/core/config/network.ts @@ -20,6 +20,6 @@ export const Networks: Record = { rbfTimes: 5, boardcastTimes: 15, }, - hubAddress: "0xA400A766cac75EF3Eb605f23a6a473dB5d4AbBBf", + hubAddress: "0xe0CAb641c88d7E00D4fEfC91aD87657FFd2Af79E", }, } diff --git a/src/core/contracts/hub.ts b/src/core/contracts/hub.ts new file mode 100644 index 0000000..239225b --- /dev/null +++ b/src/core/contracts/hub.ts @@ -0,0 +1,17 @@ +import { Contract, ethers } from "ethers"; +import { GOEFactoryAbi } from "../config/index.js"; +import { getNetworkConfig, randomRPC } from "./utils.js"; + +export async function getHubContract( + chainId: number, + signer: ethers.Signer | null = null +): Promise { + const network = getNetworkConfig(chainId); + if (signer) { + return new Contract(network.hubAddress, GOEFactoryAbi, signer); + } else { + const rpcUrl = randomRPC(network.rpc); + const provider = new ethers.JsonRpcProvider(rpcUrl); + return new Contract(network.hubAddress, GOEFactoryAbi, provider); + } +} diff --git a/src/core/contracts/index.ts b/src/core/contracts/index.ts new file mode 100644 index 0000000..1813a69 --- /dev/null +++ b/src/core/contracts/index.ts @@ -0,0 +1,4 @@ +export * from './hub.js'; +export * from './repo.js'; +export * from './resolver/index.js'; +export * from './utils.js'; diff --git a/src/core/contracts/repo.ts b/src/core/contracts/repo.ts new file mode 100644 index 0000000..dfced3a --- /dev/null +++ b/src/core/contracts/repo.ts @@ -0,0 +1,19 @@ +import { Contract, ethers } from "ethers"; +import { GOERepoAbi } from "../config/index.js"; +import { validateAddress, randomRPC, getNetworkConfig } from "./utils.js"; + +export async function getRepoContract( + repoAddress: string, + chainId: number, + signer: ethers.Signer | null = null +): Promise { + validateAddress(repoAddress, "repoAddress"); + if (signer) { + return new Contract(repoAddress, GOERepoAbi, signer); + } else { + const network = getNetworkConfig(chainId); + const rpcUrl = randomRPC(network.rpc); + const provider = new ethers.JsonRpcProvider(rpcUrl); + return new Contract(repoAddress, GOERepoAbi, provider); + } +} diff --git a/src/core/contracts/resolver/index.ts b/src/core/contracts/resolver/index.ts new file mode 100644 index 0000000..45e31a4 --- /dev/null +++ b/src/core/contracts/resolver/index.ts @@ -0,0 +1,47 @@ +import { ethers, ZeroAddress } from "ethers"; +import { getHubContract } from "../hub.js"; + +export async function resolveRepoAddress( + chainId: number, + input: string, + defaultOwner?: string +): Promise { + // 1. address + const strInput: string = input; // fix ts build bug + if (ethers.isAddress(strInput)) { + return strInput; + } + + // 2. owner/repo + if (input.includes("/")) { + const [owner, repoName] = input.split("/"); + if (!owner || !repoName) { + throw new Error(`Invalid repo identifier: "${input}"`); + } + if (ethers.isAddress(repoName)) { + throw new Error( + `Invalid repo identifier "${input}". Use repo address directly.` + ); + } + return await resolveByName(chainId, owner, repoName); + } + + // 3. repoName only + if (!defaultOwner) { + throw new Error(`Owner is required to resolve repo name "${input}"`); + } + return await resolveByName(chainId, defaultOwner, input); +} + +async function resolveByName( + chainId: number, + owner: string, + repoName: string +): Promise { + const factory = await getHubContract(chainId); + const repo = await factory.getRepoByName(owner, repoName); + if (!repo || repo === ZeroAddress) { + throw new Error(`Repository "${owner}/${repoName}" not found`); + } + return repo; +} diff --git a/src/core/contracts/utils.ts b/src/core/contracts/utils.ts new file mode 100644 index 0000000..2609753 --- /dev/null +++ b/src/core/contracts/utils.ts @@ -0,0 +1,20 @@ +import { ethers } from "ethers"; +import { Networks } from "../config/index.js"; + +export function validateAddress(address: string, label = "address") { + if (!ethers.isAddress(address)) { + throw new Error(`Invalid ${label}: ${address}`); + } +} + +export function randomRPC(rpcs: string[]): string { + return rpcs[Math.floor(Math.random() * rpcs.length)]; +} + +export function getNetworkConfig(chainId: number) { + const config = Networks[chainId]; + if (!config) { + throw new Error(`Unsupported chain ID: ${chainId}.`); + } + return config; +} diff --git a/src/core/index.ts b/src/core/index.ts index a3224d2..95f66ca 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,3 +1,4 @@ // core/index.ts export * from './config/index.js'; +export * from './contracts/index.js'; export { WalletManager } from './wallet/manager.js'; From 57396128dc097ebce14d743f6e838f199bc9380e Mon Sep 17 00:00:00 2001 From: iteye Date: Wed, 7 Jan 2026 16:19:48 +0800 Subject: [PATCH 2/4] helper support repo name --- src/helper/core/goe.ts | 3 +-- src/helper/utils/index.ts | 35 ++++++++++++++++++++--------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/helper/core/goe.ts b/src/helper/core/goe.ts index 23333ad..49b969f 100644 --- a/src/helper/core/goe.ts +++ b/src/helper/core/goe.ts @@ -9,14 +9,13 @@ import { GOEProtocol, FetchRef, PackCreationResult, PushRecord, PushRef, Ref } from "../types/index.js"; -import { GOERepoAbi, WalletManager } from "../../core/index.js"; +import { GOERepoAbi, WalletManager, randomRPC } from "../../core/index.js"; import { createCommitBoundaryPacks, findCommonAncestor, findMatchingLocalBranch, getOidFromRef, log, - randomRPC, runGitPackFromFile } from "../utils/index.js"; import { ContractDriver } from "./contract.js"; diff --git a/src/helper/utils/index.ts b/src/helper/utils/index.ts index d4b7616..0f62268 100644 --- a/src/helper/utils/index.ts +++ b/src/helper/utils/index.ts @@ -2,7 +2,7 @@ import URLParse from "url-parse" import pLimit from "p-limit"; import { ethers } from "ethers"; -import { Networks } from "../../core/index.js" +import { Networks, resolveRepoAddress, WalletManager } from "../../core/index.js" import { GOEProtocol, NegotiationResult, PushRecord } from "../types/index.js"; import { getLocalCommitOids } from "./git-helper.js"; import { ContractDriver } from "../core/contract.js"; @@ -10,31 +10,36 @@ import { ContractDriver } from "../core/contract.js"; export * from './log.js'; export * from './git-helper.js'; +function getOwner(): string { + const address = WalletManager.getDefaultAddress(); + if (!address) throw new Error(`Wallet not found. Please run 'goe wallet create' to create it.`); + return address; +} + export async function parseGoeURI(uri: string): Promise { - const url = new URLParse(uri) - let hostname = url.hostname - if (!hostname || !ethers.isAddress(hostname)) { - throw new Error("invalid goe uri, no contract address") - } + const url = new URLParse(uri); - let chainId = url.port ? parseInt(url.port) : null - if (!chainId) throw new Error("invalid goe uri, no chainId") + const chainId = Number(url.port); + if (!chainId) throw new Error("invalid goe uri, no chainId"); - let netConfig = Networks[chainId] - if (!netConfig) throw new Error(`Not Support chainId: ${chainId}`) + const netConfig = Networks[chainId]; + if (!netConfig) throw new Error(`Not Support chainId: ${chainId}`); + + const repoNamespace = url.hostname + url.pathname; + const defaultOwner = getOwner(); + const repoAddress: string = await resolveRepoAddress(chainId, repoNamespace, defaultOwner); + if (!repoAddress || !ethers.isAddress(repoAddress)) { + throw new Error("invalid goe uri, no contract address"); + } return { remoteUrl: uri, - repoAddress: hostname, + repoAddress: repoAddress, chainId, netConfig, } } -export function randomRPC(rpcs: string[]): string { - return rpcs[Math.floor(Math.random() * rpcs.length)] -} - const DEFAULT_NEGOTIATION_RESULT: NegotiationResult = { commonRecord: null, commonIndex: -1, From fd05c8d4fb30c9455bf89c417db947c3941e950f Mon Sep 17 00:00:00 2001 From: iteye Date: Thu, 8 Jan 2026 13:51:36 +0800 Subject: [PATCH 3/4] Fix bug and update doc --- README.md | 30 ++++++++++++++-------------- src/cli/repo/index.ts | 6 ++++-- src/core/contracts/resolver/index.ts | 1 + 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 69ee01b..8df5fd7 100644 --- a/README.md +++ b/README.md @@ -33,16 +33,15 @@ For a deeper technical overview of GoE's architecture and on-chain Git mechanics ## `goe://` Protocol -GoE introduces a custom Git protocol to access on-chain repositories. +GoE repositories are identified on-chain by their contract addresses. The goe:// protocol lets you reference a repository in three ways: -```bash -goe://: -``` - -- `` β€” the smart contract address of the repository -- `` β€” the chain ID where the repository is deployed +| URI Format | Type | Resolution Logic | +|--------------------------------------------|-----------|------------------------------------------------| +| ```goe://:``` | Canonical | Direct access via on-chain contract address | +| ```goe://:``` | Shorthand | Resolves using your current wallet + repo name | +| ```goe:///:``` | Full Path | Resolves via any owner address + repo name | -> This protocol is automatically handled by the Git Helper installed with `goe-cli`. No additional setup is required. +> **Note:** `` refers to the repository's smart contract; `` is the blockchain network ID. --- @@ -104,7 +103,8 @@ goe wallet lock ### 4. Repo Command -Create and manage on-chain repositories and permissions. +> All goe repo commands work with repositories owned by the current wallet. You can refer to a repository either by its +> name or by its contract address. - **Create a repository** ```bash @@ -120,18 +120,18 @@ goe repo list [--chain-id ] - **List branches** ```bash -goe repo branches [--chain-id ] +goe repo branches [--chain-id ] ``` - **Set default branch** ```bash -goe repo default-branch [--chain-id ] +goe repo default-branch [--chain-id ] ``` - **Grant / Revoke push access** ```bash -goe repo grant-push [--chain-id ] -goe repo revoke-push [--chain-id ] +goe repo grant-push [--chain-id ] +goe repo revoke-push [--chain-id ] ``` ### 5. Example Workflow @@ -182,12 +182,12 @@ GOE_GAS_INC_PCT=10 git push -u origin main #### 5). Set the default branch ```bash # Only needed if you want to change it later. -goe repo default-branch master --chain-id 11155111 +goe repo default-branch master --chain-id 11155111 ``` #### 6). Grant collaborator push access ```bash -goe repo grant-push --chain-id 11155111 +goe repo grant-push --chain-id 11155111 ``` diff --git a/src/cli/repo/index.ts b/src/cli/repo/index.ts index 23b2988..ebfb12f 100644 --- a/src/cli/repo/index.ts +++ b/src/cli/repo/index.ts @@ -34,7 +34,9 @@ repoCmd logger.info(`Creating repository "${name}" on chain ${chainId}...`); const repoAddress = await Factory.createRepo(name, chainId); logger.success(`Repository created successfully: ${repoAddress}`); - logger.normal(`πŸ”— Access via: goe://${repoAddress}:${chainId}`); + logger.normal(`πŸ”— Access via:`); + logger.normal(` goe://${repoAddress}:${chainId} (recommended)`); + logger.normal(` goe://${name}:${chainId} (local alias, current wallet only)`); } catch (e: any) { logger.error(`Failed to create repository: ${e.message}`); } @@ -57,7 +59,7 @@ repoCmd logger.success(`Found ${repos.length} repositories:`); repos.forEach((repo: RepoInfo, idx: number) => { - logger.normal(`${idx + 1}. ${repo.name} (${repo.address}) Created: ${repo.creationTime.toLocaleString()}`); + logger.normal(`${idx + 1}. ${repo.address} (${repo.name}) Created: ${repo.creationTime.toLocaleString()}`); }); } catch (e: any) { logger.error(`Failed to fetch repositories: ${e.message}`); diff --git a/src/core/contracts/resolver/index.ts b/src/core/contracts/resolver/index.ts index 45e31a4..28eaebb 100644 --- a/src/core/contracts/resolver/index.ts +++ b/src/core/contracts/resolver/index.ts @@ -39,6 +39,7 @@ async function resolveByName( repoName: string ): Promise { const factory = await getHubContract(chainId); + repoName = ethers.hexlify(ethers.toUtf8Bytes(repoName)); const repo = await factory.getRepoByName(owner, repoName); if (!repo || repo === ZeroAddress) { throw new Error(`Repository "${owner}/${repoName}" not found`); From 13fc28deb4615a993b3998c92fec58aff4a28f5b Mon Sep 17 00:00:00 2001 From: iteye Date: Thu, 8 Jan 2026 13:52:56 +0800 Subject: [PATCH 4/4] update doc --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8df5fd7..e4c12f1 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,11 @@ For a deeper technical overview of GoE's architecture and on-chain Git mechanics GoE repositories are identified on-chain by their contract addresses. The goe:// protocol lets you reference a repository in three ways: -| URI Format | Type | Resolution Logic | -|--------------------------------------------|-----------|------------------------------------------------| -| ```goe://:``` | Canonical | Direct access via on-chain contract address | -| ```goe://:``` | Shorthand | Resolves using your current wallet + repo name | -| ```goe:///:``` | Full Path | Resolves via any owner address + repo name | +| URI Format | Type | Resolution Logic | +|--------------------------------------------|-----------|------------------------------------------------------| +| ```goe://:``` | Canonical | Direct access via on-chain contract address | +| ```goe://:``` | Shorthand | Resolves via the current wallet and repository name | +| ```goe:///:``` | Full Path | Resolves via any owner’s address and repository name | > **Note:** `` refers to the repository's smart contract; `` is the blockchain network ID.