Talis: consolidating five chain codebases
Published
At Talis, the same marketplace product was maintained across five chain-specific codebases. A shared bug fix could require five pull requests, five reviews, and coordinated releases. The main reliability risk was a change reaching some chains and being missed on others.
My role: Freelance Full-stack Developer, 2021–April 2025. I worked on consolidating the application around typed chain configuration, capability flags, and wallet adapters.
The problem was repeated product code
The marketplace served communities on Terra 1, Terra 2, Injective, Xion, and Orai. Each chain had different addresses, endpoints, currency metadata, and wallet configuration, while much of the user-facing behavior was shared.
Adding a chain by copying a repository initially made the integration easy to isolate. Over time, the copies diverged. A UI correction or contract-interaction fix had to be identified as shared, ported into the other repositories, and verified again. The coordination burden grew with every common change.
The goal was to put shared behavior in one codebase and make chain differences explicit. That still leaves chain-specific configuration, deployments, and verification; it removes the need to manually keep several copies of the same application logic aligned.
Put chain differences behind a typed boundary
The first boundary was a configuration schema. Each chain entry described identifiers, endpoints, contracts, supported capabilities, currency metadata, and wallet adapters. This reduced the need to search through product code for network-specific constants.
A simplified excerpt shows the shape of that boundary:
interface ChainConfig {
chainId: string
rpc: string
contracts: {
marketplace: string
nftFactory: string
}
features: {
auctions: boolean
lazyMinting: boolean
collections: boolean
}
}
Types make missing or incompatible fields visible during development. Runtime validation has a separate role: checking values that come from configuration or the environment. Neither replaces verifying that an endpoint is healthy or a contract address points to the intended deployment.
Ask about a capability, not a chain name
The application had branches that enabled behavior for particular networks. Those branches made the next integration depend on finding every place where chain identity affected a feature.
The replacement moved that decision into configuration:
// Before: product behavior depends on network identity.
if (chainId === 'injective-1' || chainId === 'xion-1') {
showAuctionUI()
}
// After: product behavior depends on a declared capability.
if (chain.features.auctions) {
showAuctionUI()
}
The application now asks a question it actually needs answered: whether auctions are available. The configuration determines which chains satisfy that condition. A new chain can declare the capability without requiring another identity check in the UI.
This is also a constraint on future changes. If developers bypass configuration and add new chain-name branches, the old coupling returns. The schema only helps when it remains the boundary used by the product.
Keep wallet integration close to configuration
Wallet registration needs chain identifiers, address prefixes, gas settings, currency details, and endpoints. I worked on moving that information into a configuration-driven adapter layer so integrations could consume a consistent chain definition.
The approach still needs room for behavior that does not fit a shared flag. An adapter provides an explicit place for that difference. Trying to express every exception as a boolean would eventually make the configuration harder to understand than the code it replaced.
Make verification part of adding a chain
The unified structure made the integration surface easier to enumerate: contract addresses, endpoint health, wallet registration, feature availability, explorer links, and smoke checks for enabled capabilities.
This matters because a shared implementation increases the reach of a change. A fix is authored once, but its behavior still needs checking across supported configurations. Consolidation reduces duplicated work; it does not eliminate the test matrix or guarantee a coordinated deployment.
For a marketplace with on-chain royalty behavior, configuration correctness also affects the product promise. A deployment pointing at the wrong address can be incorrect even when the application code and its types are valid.
What changed
Shared fixes could be made in one codebase instead of manually ported between five repositories. Chain differences became reviewable configuration and adapter changes, and the requirements for an integration became explicit.
The architectural result is fewer independent copies of shared logic and a clearer boundary for variation. It directly addresses the failure mode that prompted the work: a shared fix being omitted from a sibling repository.
The tradeoff I would carry forward
A common codebase works well when the products share enough behavior to justify it. It also makes accidental coupling more consequential. The useful discipline is to keep configuration specific, adapters small, and verification tied to the capabilities each deployment actually supports.
For similar work, I would define the integration checklist alongside the first configuration schema. It makes the intended boundary reviewable early and provides a practical way to tell whether the next integration fits the model.