Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions packages/sdk/contractkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,12 @@
"@celo/connect": "^7.0.0",
"@celo/utils": "^8.0.3",
"@celo/wallet-local": "^8.0.1",
"@types/bn.js": "^5.1.0",
"@types/debug": "^4.1.5",
"bignumber.js": "^9.0.0",
"debug": "^4.1.1",
"fp-ts": "2.16.9",
"semver": "^7.7.2",
"web3": "1.10.4",
"web3-core-helpers": "1.10.4"
"viem": "^2.33.2"
},
"devDependencies": {
"@celo/celo-devchain": "^7.0.0",
Expand All @@ -50,7 +48,6 @@
"@jest/test-sequencer": "^30.0.2",
"@types/debug": "^4.1.5",
"@types/node": "18.7.16",
"bn.js": "^5.1.0",
"cross-fetch": "3.1.5",
"fetch-mock": "^10.0.7",
"jest": "^29.7.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Compile-time type safety verification for strongly-typed contract methods.
*
* This file is NOT a runtime test. It uses TypeScript's type system to verify
* that .read enforces correct method names and argument types
* at compile time. The @ts-expect-error directives verify that intentional
* type errors are caught by the TypeScript compiler.
*
* Run with: yarn workspace @celo/contractkit run build
*/

import { accountsABI } from '@celo/abis'
import type { CeloContract } from '@celo/connect'

// Declare a typed Accounts contract with const-typed ABI
declare const accountsContract: CeloContract<typeof accountsABI>

// ============================================================================
// Tests 1-4: CeloContract .read property type safety
// ============================================================================
// CeloContract provides a .read namespace with type-safe view methods.
// This section verifies that .read property access works correctly.

// Test 1: .read.isAccount resolves to correct function type
// 'isAccount' is a valid view method on Accounts. Should compile without error.
void accountsContract.read.isAccount

// Test 2: .read with correct method name is callable
// Verify that the function can be called with correct arguments.
// 'isAccount' takes an address parameter and returns boolean.
const isAccountFn = accountsContract.read.isAccount
void isAccountFn

// Test 3: .read rejects invalid method names
// 'nonExistentFunction' is not a valid method on Accounts contract.
// @ts-expect-error - 'nonExistentFunction' is not a valid method name
void accountsContract.read.nonExistentFunction

// Test 4: .read.createAccount should fail (send-only method)
// 'createAccount' is a send method, not a view method. .read should reject it.
// @ts-expect-error - 'createAccount' is not a view/pure method
void accountsContract.read.createAccount

// ============================================================================
// Tests 5-8: CeloContract (GetContractReturnType) compatibility
// ============================================================================

// CeloContract uses viem's GetContractReturnType.
// The ContractLike<TAbi> parameter type ensures it works with .read.
declare const celoContract: CeloContract<typeof accountsABI>

// Test 5: .read.isAccount with CeloContract compiles
// 'isAccount' is a valid view method on Accounts. Should compile without error.
void celoContract.read.isAccount

// Test 6: .read with CeloContract rejects incorrect method name
// @ts-expect-error - 'nonExistentFunction' is not a valid method name on Accounts contract
void celoContract.read.nonExistentFunction

// Test 7: .read.createAccount should fail (send-only method)
// 'createAccount' is a send method, not a view method. .read should reject it.
// @ts-expect-error - 'createAccount' is not a view/pure method
void celoContract.read.createAccount
12 changes: 7 additions & 5 deletions packages/sdk/contractkit/src/address-registry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { newRegistry, Registry } from '@celo/abis/web3/Registry'
import { registryABI } from '@celo/abis'
import { NULL_ADDRESS, StrongAddress } from '@celo/base/lib/address'
import { Connection } from '@celo/connect'
import { Connection, type ContractRef } from '@celo/connect'
import debugFactory from 'debug'
import { CeloContract, RegisteredContracts, stripProxy } from './base'

Expand All @@ -21,12 +21,12 @@ export class UnregisteredError extends Error {
* @param connection – an instance of @celo/connect {@link Connection}
*/
export class AddressRegistry {
private readonly registry: Registry
private readonly registry: ContractRef
private readonly cache: Map<CeloContract, StrongAddress> = new Map()

constructor(readonly connection: Connection) {
this.cache.set(CeloContract.Registry, REGISTRY_CONTRACT_ADDRESS)
this.registry = newRegistry(connection.web3, REGISTRY_CONTRACT_ADDRESS)
this.registry = connection.getCeloContract(registryABI as any, REGISTRY_CONTRACT_ADDRESS)
}

/**
Expand All @@ -35,7 +35,9 @@ export class AddressRegistry {
async addressFor(contract: CeloContract): Promise<StrongAddress> {
if (!this.cache.has(contract)) {
debug('Fetching address from Registry for %s', contract)
const address = await this.registry.methods.getAddressForString(stripProxy(contract)).call()
const address = (await (this.registry as any).read.getAddressForString([
stripProxy(contract),
])) as string

debug('Fetched address %s', address)
if (!address || address === NULL_ADDRESS) {
Expand Down
5 changes: 0 additions & 5 deletions packages/sdk/contractkit/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,6 @@ export type CeloTokenContract =
| StableTokenContract
| CeloContract.CeloToken
| CeloContract.GoldToken
/**
* Deprecated alias for CeloTokenContract.
* @deprecated Use CeloTokenContract instead
*/
export type CeloToken = CeloTokenContract

export const AllContracts = Object.values(CeloContract) as CeloContract[]
const AuxiliaryContracts = [CeloContract.MultiSig, CeloContract.ERC20]
Expand Down
5 changes: 2 additions & 3 deletions packages/sdk/contractkit/src/celo-tokens.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import Web3 from 'web3'
import { CeloContract } from './base'
import { CeloTokenInfo, CeloTokens, StableToken, Token } from './celo-tokens'
import { ContractKit, newKitFromWeb3 } from './kit'
import { ContractKit, newKit } from './kit'

describe('CeloTokens', () => {
let kit: ContractKit
let celoTokens: CeloTokens

beforeEach(() => {
kit = newKitFromWeb3(new Web3('http://localhost:8545'))
kit = newKit('http://localhost:8545')
celoTokens = kit.celoTokens
})

Expand Down
21 changes: 13 additions & 8 deletions packages/sdk/contractkit/src/contract-cache.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Connection } from '@celo/connect'
import Web3 from 'web3'
import { getProviderForKit } from './setupForKits'
import { CeloContract } from '.'
import { AddressRegistry } from './address-registry'
import { ValidWrappers, WrapperCache } from './contract-cache'
import { Web3ContractCache } from './web3-contract-cache'
import { ContractCache } from './contract-factory-cache'
import * as crypto from 'crypto'

const TestedWrappers: ValidWrappers[] = [
CeloContract.GoldToken,
Expand All @@ -13,14 +14,18 @@ const TestedWrappers: ValidWrappers[] = [
CeloContract.LockedCelo,
]

function createMockProvider() {
return getProviderForKit('http://localhost:8545')
}

function newWrapperCache() {
const web3 = new Web3('http://localhost:8545')
const connection = new Connection(web3)
const provider = createMockProvider()
const connection = new Connection(provider)
const registry = new AddressRegistry(connection)
const web3ContractCache = new Web3ContractCache(registry)
const nativeContractCache = new ContractCache(registry)
const AnyContractAddress = '0xe832065fb5117dbddcb566ff7dc4340999583e38'
jest.spyOn(registry, 'addressFor').mockResolvedValue(AnyContractAddress)
const contractCache = new WrapperCache(connection, web3ContractCache, registry)
const contractCache = new WrapperCache(connection, nativeContractCache, registry)
return contractCache
}

Expand All @@ -36,8 +41,8 @@ describe('getContract()', () => {
}

test('should create a new instance when an address is provided', async () => {
const address1 = Web3.utils.randomHex(20)
const address2 = Web3.utils.randomHex(20)
const address1 = '0x' + crypto.randomBytes(20).toString('hex')
const address2 = '0x' + crypto.randomBytes(20).toString('hex')
const contract1 = await contractCache.getContract(CeloContract.MultiSig, address1)
const contract2 = await contractCache.getContract(CeloContract.MultiSig, address2)
expect(contract1?.address).not.toEqual(contract2?.address)
Expand Down
11 changes: 5 additions & 6 deletions packages/sdk/contractkit/src/contract-cache.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { IERC20 } from '@celo/abis/web3/IERC20'
import { Connection } from '@celo/connect'
import { AddressRegistry } from './address-registry'
import { CeloContract } from './base'
import { ContractCacheType } from './basic-contract-cache-type'
import { StableToken, stableTokenInfos } from './celo-tokens'
import { Web3ContractCache } from './web3-contract-cache'
import { ContractCache } from './contract-factory-cache'
import { AccountsWrapper } from './wrappers/Accounts'
import { AttestationsWrapper } from './wrappers/Attestations'
import { ElectionWrapper } from './wrappers/Election'
Expand Down Expand Up @@ -75,7 +74,7 @@ interface WrapperCacheMap {
[CeloContract.Election]?: ElectionWrapper
[CeloContract.EpochManager]?: EpochManagerWrapper
[CeloContract.EpochRewards]?: EpochRewardsWrapper
[CeloContract.ERC20]?: Erc20Wrapper<IERC20>
[CeloContract.ERC20]?: Erc20Wrapper
[CeloContract.Escrow]?: EscrowWrapper
[CeloContract.FederatedAttestations]?: FederatedAttestationsWrapper
[CeloContract.FeeCurrencyDirectory]?: FeeCurrencyDirectoryWrapper
Expand Down Expand Up @@ -111,7 +110,7 @@ export class WrapperCache implements ContractCacheType {
private wrapperCache: WrapperCacheMap = {}
constructor(
readonly connection: Connection,
readonly _web3Contracts: Web3ContractCache,
readonly _contracts: ContractCache,
readonly registry: AddressRegistry
) {}

Expand Down Expand Up @@ -190,7 +189,7 @@ export class WrapperCache implements ContractCacheType {
*/
public async getContract<C extends ValidWrappers>(contract: C, address?: string) {
if (this.wrapperCache[contract] == null || address !== undefined) {
const instance = await this._web3Contracts.getContract<C>(contract, address)
const instance = await this._contracts.getContract(contract, address)
if (contract === CeloContract.SortedOracles) {
const Klass = WithRegistry[CeloContract.SortedOracles]
this.wrapperCache[CeloContract.SortedOracles] = new Klass(
Expand All @@ -213,7 +212,7 @@ export class WrapperCache implements ContractCacheType {
}

public invalidateContract<C extends ValidWrappers>(contract: C) {
this._web3Contracts.invalidateContract(contract)
this._contracts.invalidateContract(contract)
this.wrapperCache[contract] = undefined
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import { Connection } from '@celo/connect'
import { testWithAnvilL2 } from '@celo/dev-utils/anvil-test'
import Web3 from 'web3'
import { AddressRegistry } from './address-registry'
import { AllContracts } from './index'
import { Web3ContractCache } from './web3-contract-cache'
import { ContractCache } from './contract-factory-cache'

testWithAnvilL2('web3-contract-cache', (web3: Web3) => {
function newWeb3ContractCache() {
const connection = new Connection(web3)
testWithAnvilL2('provider-contract-cache', (provider) => {
function newContractCache() {
const connection = new Connection(provider)
const registry = new AddressRegistry(connection)
const AnyContractAddress = '0xe832065fb5117dbddcb566ff7dc4340999583e38'
jest.spyOn(registry, 'addressFor').mockResolvedValue(AnyContractAddress)
return new Web3ContractCache(registry)
return new ContractCache(registry)
}

describe('getContract()', () => {
const contractCache = newWeb3ContractCache()
const contractCache = newContractCache()

for (const contractName of AllContracts) {
test(`SBAT get ${contractName}`, async () => {
Expand All @@ -26,7 +25,7 @@ testWithAnvilL2('web3-contract-cache', (web3: Web3) => {
}
})
test('should cache contracts', async () => {
const contractCache = newWeb3ContractCache()
const contractCache = newContractCache()
for (const contractName of AllContracts) {
const contract = await contractCache.getContract(contractName)
const contractBis = await contractCache.getContract(contractName)
Expand All @@ -35,7 +34,7 @@ testWithAnvilL2('web3-contract-cache', (web3: Web3) => {
})
describe('getLockedCelo()', () => {
it('returns the LockedCelo contract', async () => {
const contractCache = newWeb3ContractCache()
const contractCache = newContractCache()
const contract = await contractCache.getLockedCelo()
expect(contract).not.toBeNull()
expect(contract).toBeDefined()
Expand All @@ -44,7 +43,7 @@ testWithAnvilL2('web3-contract-cache', (web3: Web3) => {
})
describe('getCeloToken()', () => {
it('returns the CELO token contract', async () => {
const contractCache = newWeb3ContractCache()
const contractCache = newContractCache()
const contract = await contractCache.getCeloToken()
expect(contract).not.toBeNull()
expect(contract).toBeDefined()
Expand Down
Loading
Loading