lamlv2305: $#folder:packages help me config yarn workspace, so i want #file:multicall3.ts can import interface from package @holdstation/worldchain-sdk at #folder:core
GitHub Copilot: To configure Yarn workspaces so that your package in ether-v5 can import interfaces from @holdstation/worldchain-sdk
in core, follow these steps:
- Create a root package.json (if you don't have one) at worldchain-sdk with a
workspaces
field:
{
"private": true,
"workspaces": [
"packages/*"
]
}
Update
@holdstation/worldchain-sdk
package name
Your package.json already has"name": "@holdstation/worldchain-sdk"
, which is correct.Add dependency in
ether-v5
In package.json, add@holdstation/worldchain-sdk
as a dependency:
{
// ...existing code...
"dependencies": {
"@holdstation/worldchain-sdk": "*"
// ...other dependencies...
}
// ...existing code...
}
Install dependencies
From your monorepo root, run:yarn install
Import interfaces
Now, in your code (like in packages/ether-v5/src/multicall3.ts), you can import from@holdstation/worldchain-sdk
as you are already doing.
Summary:
- Ensure the root package.json has a
workspaces
field. - Reference
@holdstation/worldchain-sdk
in your dependent package's package.json. - Run
yarn install
at the root. - Use imports as normal.
Let me know if you need help with a specific step!
lamlv2305: do i need to build core before run ?
GitHub Copilot: Yes, you typically need to build @holdstation/worldchain-sdk
before running or building @holdstation/worldchain-ethers-v5
, especially if core
is written in TypeScript or needs a build step to generate output files (like dist/
or lib/
).
Why?
When you import from @holdstation/worldchain-sdk
, your code expects the compiled output (e.g., .js
and .d.ts
files) to be present. If you haven't built core
, those files may not exist, causing import errors.
Best practice:
- Build
core
first. - Then build or run
ether-v5
.
Tip:
You can automate this with a root-level script using Yarn workspaces or Lerna, or use yarn workspaces run to build all packages in the correct order:
yarn workspaces run build
This will run the build
script in all your packages, respecting dependency order.
lamlv2305: how to let this package using @holdstation/worldchain-sdk
I'm using yarn workspace
GitHub Copilot: To use @holdstation/worldchain-sdk
in your @holdstation/worldchain-ethers-v5
package with Yarn workspaces, follow these steps:
Monorepo structure
Your folders should look like:/worldchain-sdk/ package.json // <-- root, with "workspaces" /packages/ /core/ // <-- @holdstation/worldchain-sdk package.json /ether-v5/ // <-- @holdstation/worldchain-ethers-v5 package.json
Root package.json
Make sure your root package.json has:{ "private": true, "workspaces": [ "packages/*" ] }
Dependency in
ether-v5
Your package.json already has:"dependencies": { "@holdstation/worldchain-sdk": "*", "ethers": "^5.8.0" }
This is correct.
Install dependencies
From the root of your monorepo, run:yarn install
Yarn will link
@holdstation/worldchain-sdk
from core into node_modules ofether-v5
.Build order
Ifcore
needs to be built (e.g., TypeScript), build it first:cd packages/core yarn build
Then build
ether-v5
:cd ../ether-v5 yarn build
Or, from the root, run:
yarn workspaces run build
This will build all packages in dependency order.
Import in code
Now you can import from@holdstation/worldchain-sdk
in your TypeScript files:import { SomeInterface } from "@holdstation/worldchain-sdk";
Summary:
- Use Yarn workspaces in the root.
- Add
@holdstation/worldchain-sdk
as a dependency. - Run
yarn install
at the root. - Build
core
beforeether-v5
. - Import as normal in your code.
Let me know if you need help with a specific step!
lamlv2305: decodeFunctionResult(functionFragment: string, data: string): Result { const iface = new ethers.utils.Interface([functionFragment]); return iface.decodeFunctionResult(functionFragment, data); }
does it correct ?
GitHub Copilot: Your implementation is almost correct for simple use cases, but it has a subtle issue:
ethers.utils.Interface([functionFragment])
expects an ABI fragment (like a function signature), but when you calldecodeFunctionResult(functionFragment, data)
, the first argument should be the function name (not the full fragment/signature).
If functionFragment
is a full signature (e.g., "function balanceOf(address) view returns (uint256)"
), you should extract the function name (e.g., "balanceOf"
) when calling decodeFunctionResult
.
Example fix:
decodeFunctionResult(functionFragment: string, data: string): Result {
const iface = new ethers.utils.Interface([functionFragment]);
// Extract function name from the fragment
const functionName = iface.fragments[0].name;
return iface.decodeFunctionResult(functionName, data);
}
Summary:
- If you pass a full function fragment, extract the function name for decoding.
- If you already pass just the function name, your original code is fine.
Recommendation:
Use the fix above for robustness.
lamlv2305: review encodeFunctionData
GitHub Copilot: Your encodeFunctionData
implementation works if functionFragment
is a full function fragment (e.g., "function balanceOf(address) view returns (uint256)"
).
However, iface.encodeFunctionData
expects the function name (e.g., "balanceOf"
) as the first argument, not the full fragment.
Current code:
encodeFunctionData(functionFragment: string, values?: any[]): string {
const iface = new ethers.utils.Interface([functionFragment]);
return iface.encodeFunctionData(functionFragment, values);
}
Issue:
If you pass the full fragment as both the ABI and the function name, it will throw an error.
Recommended fix:
Extract the function name from the fragment, similar to your decodeFunctionResult
:
encodeFunctionData(functionFragment: string, values?: any[]): string {
const iface = new ethers.utils.Interface([functionFragment]);
const functionName = iface.fragments[0].name;
return iface.encodeFunctionData(functionName, values);
}
Summary:
- Extract the function name from the fragment before calling
encodeFunctionData
. - This makes your method robust for full function fragments.
lamlv2305: make encodeFunctionData
for viem please
GitHub Copilot: ````typescript import { encodeFunctionData as viemEncodeFunctionData, parseAbi } from 'viem'
export class ViemClient implements Client { // ...existing code...
encodeFunctionData(functionFragment: string, values?: any[]): string { const abi = parseAbi([functionFragment]); const functionName = abi[0].name; return viemEncodeFunctionData({ abi, functionName, args: values ?? [], }); }
// ...existing code... }
**Notes:**
- This uses `parseAbi` to parse the function fragment and extract the function name.
- Uses `viem`'s `encodeFunctionData` utility for encoding.
- Assumes `functionFragment` is a full function signature (e.g., `"function balanceOf(address) view returns (uint256)"`).
lamlv2305: you are liar
Property 'name' does not exist on type 'never'
Argument of type '{ abi: never; functionName: any; args: any[]; }' is not assignable to parameter of type 'never'.ts(2345)
GitHub Copilot: Thank you for pointing out the error. The issue is that `parseAbi([functionFragment])` returns `never[]` if the input is not a valid ABI fragment or if the types are not inferred correctly.
**How to fix:**
You should use the correct ABI type for `parseAbi` and ensure the fragment is a valid function signature.
Here’s a robust way to implement `encodeFunctionData` for Viem:
````typescript
import { AbiFunction, parseAbi, encodeFunctionData as viemEncodeFunctionData } from 'viem'
encodeFunctionData(functionFragment: string, values?: any[]): string {
// parseAbi returns Abi[], so we cast it to AbiFunction[]
const abi = parseAbi([functionFragment]) as AbiFunction[];
if (!abi.length || !abi[0].name) {
throw new Error('Invalid function fragment');
}
const functionName = abi[0].name;
return viemEncodeFunctionData({
abi,
functionName,
args: values ?? [],
});
}
Key points:
- Cast the result of
parseAbi
toAbiFunction[]
. - Check that the ABI and function name exist.
- Now TypeScript will recognize the
name
property and the types will match.
Let me know if you need further help!
lamlv2305: how about decode ?
GitHub Copilot: Here’s how you can implement decodeFunctionResult
for Viem using parseAbi
and decodeFunctionResult
from viem
:
import { AbiFunction, parseAbi, decodeFunctionResult as viemDecodeFunctionResult } from 'viem'
decodeFunctionResult(abi: string[], functionFragment: string, data: string): Result {
const parsedAbi = parseAbi(abi) as AbiFunction[];
if (!parsedAbi.length || !parsedAbi[0].name) {
throw new Error('Invalid function fragment');
}
const functionName = parsedAbi[0].name;
return viemDecodeFunctionResult({
abi: parsedAbi,
functionName,
data,
});
}
Notes:
- This assumes
Result
matches the return type ofviemDecodeFunctionResult
. - If your
Result
type is different, you may need to adapt the return value accordingly. - Always ensure the ABI and function name are valid before decoding.
lamlv2305: can u convert to interface Result extends ReadonlyArray<any> { readonly [key: string]: any; }
GitHub Copilot: Certainly! Here’s how you can convert your Result
type to a TypeScript interface:
export interface Result extends ReadonlyArray<any> {
readonly [key: string]: any;
}
Usage:
- This interface allows
Result
to behave like an array, but also have additional named properties (like ethers.js call results). - You can now import and use
Result
as an interface in your TypeScript code.
If you want to replace the existing type declaration, just swap interface
for type
in your code as shown above.
lamlv2305: Type 'unknown' must have a 'Symbol.iterator' method that returns an iterator.ts(2488)
GitHub Copilot: The error occurs because Viem's readContract
return types are not properly typed as iterables for destructuring. Let's fix the aggregate
method:
import { Aggregate, Aggregate3, Multicall3, abi, config } from '@holdstation/worldchain-sdk'
import { createPublicClient, parseAbi } from 'viem'
export class ViemMulticall3 implements Multicall3 {
private readonly address: `0x${string}`
constructor(private publicClient: ReturnType<typeof createPublicClient>) {
this.address = config.multicall3Address as `0x${string}`
}
async aggregate(calls: Aggregate['request'][]): Promise<Aggregate['response']> {
const result = await this.publicClient.readContract({
address: this.address,
abi: parseAbi(abi.MULTICALL3_ABI),
functionName: 'aggregate',
args: [calls],
});
// Cast the result to the expected tuple type
const resultArray = result as [bigint, { returnData: string }[]];
const blockNumber = resultArray[0];
const returnStructs = resultArray[1];
return [blockNumber, returnStructs.map((c) => c.returnData)];
}
async aggregate3(calls: Aggregate3['request'][]): Promise<Aggregate3['response'][]> {
const results = await this.publicClient.readContract({
address: this.address,
abi: parseAbi(abi.MULTICALL3_ABI),
functionName: 'aggregate3',
args: [calls],
}) as { success: boolean, returnData: string }[];
return results.map((item) => ({
blockNumber: 0n, // Multicall3 aggregate3 usually does not return blockNumber
returnData: item.returnData,
success: item.success,
error: item.success ? undefined : 'Call failed',
}));
}
}
The key changes:
- Don't use array destructuring directly on the result
- Cast the result to the expected type
- Access the tuple elements by index
- Then extract and map the data as needed
This ensures TypeScript knows the shape of the data and can verify the operations are valid.
lamlv2305: async aggregate3(calls: Aggregate3["request"][]): Promise<Aggregate3["response"][]> { const [block, data] = await this.contract.aggregate3(calls);
return data.map((item: any) => {
return {
blockNumber: block,
returnData: item.returnData,
success: item.success,
error: item.error,
};
});
}
is this true for multicall3, i think they do not return block
GitHub Copilot: You're correct. For aggregate3
in Multicall3, the contract doesn't return a block number - it only returns an array of result objects.
Let's fix your implementation:
async aggregate3(calls: Aggregate3["request"][]): Promise<Aggregate3["response"][]> {
// Multicall3.aggregate3 only returns the results array, not a block number
const results = await this.contract.aggregate3(calls);
return results.map((item: any) => {
return {
blockNumber: 0, // No block number from aggregate3
returnData: item.returnData,
success: item.success,
error: item.success ? undefined : "Call failed", // Only include error if success is false
};
});
}
Important changes:
aggregate3
returns only the results array, not a tuple with block number- We've added a default block number of
0
- Added logic to only include error when success is false
You might want to get the current block number separately if needed in your implementation.
lamlv2305: why i got error when pass publicClient to new ViemMulticall3(publicClient)
GitHub Copilot: The error occurs because the ViemMulticall3
constructor expects a specific type for publicClient
, but you might be passing a slightly different type. This is likely due to how the createPublicClient
function is typed in Viem.
Here are a few ways to fix it:
- Fix the constructor type declaration:
import { type PublicClient } from 'viem'
export class ViemMulticall3 implements Multicall3 {
private readonly address: `0x${string}`
constructor(private publicClient: PublicClient) {
this.address = config.multicall3Address as `0x${string}`
}
// ...rest of the code
}
- Add a type cast in the test:
import { type PublicClient } from 'viem'
it('call balance on chain', async () => {
const multicall = new ViemMulticall3(publicClient as PublicClient)
})
The issue is likely that your constructor is using ReturnType<typeof createPublicClient>
, but you're creating the client with specific options like batch
and cacheTime
which might be narrowing the type.
Using the PublicClient
type directly (rather than ReturnType<typeof createPublicClient>
) is generally more reliable for this use case.
lamlv2305: Index signature in type 'Result' only permits reading.
GitHub Copilot: The error occurs because you're trying to modify the Result
type, but its index signature is defined as readonly
. Let's fix the toResult
method:
import { Result } from '@holdstation/worldchain-sdk'
import { Abi, decodeFunctionResult, encodeFunctionData as viemEncode } from 'viem'
export class ViemCodec implements AbiCodec {
constructor(private abi: Abi) {}
encodeFunctionData(fnName: string, args: unknown[] = []): string {
return viemEncode({ abi: this.abi, functionName: fnName, args })
}
decodeFunctionData(fnName: string, data: string): Result {
const fn = this.abi.find((f) => f.type === 'function' && f.name === fnName)
if (!fn || !('outputs' in fn)) {
throw new Error(`Function ${fnName} not found in ABI`)
}
const { result } = decodeFunctionResult({
abi: this.abi,
functionName: fnName as any,
data: data as `0x${string}`,
})
const keys = fn.outputs?.map((o) => o.name || '') ?? []
return this.toResult(result as any[], keys)
}
private toResult(values: any[], keys: string[]): Result {
// Create a new object with both array-like and named properties
const resultObj: Record<string | number, any> = {}
// Add array indices
values.forEach((val, i) => {
resultObj[i] = val
})
// Add named properties
keys.forEach((key, i) => {
if (key) { // Only add if key name exists
resultObj[key] = values[i]
}
})
// Add length property
resultObj.length = values.length
// Use Object.assign to cast to Result
return Object.assign(values.slice(), resultObj) as unknown as Result;
}
}
Key changes:
- Instead of modifying the array directly, we:
- Create a copy of the array using
slice()
- Use
Object.assign
to add the named properties - Cast the result to
Result
- Create a copy of the array using
This approach creates a new array with the desired properties, avoiding the issue with the readonly index signature.
lamlv2305: Property 'result' does not exist on type 'unknown'.
GitHub Copilot: The error occurs because the return value of decodeFunctionResult
in Viem doesn't have a guarantee that it contains a result
property. Let's fix the decodeFunctionData
method:
import { AbiCodec, Result } from '@holdstation/worldchain-sdk'
import { Abi, decodeFunctionResult, encodeFunctionData as viemEncode } from 'viem'
export class ViemCodec implements AbiCodec {
constructor(private abi: Abi) {}
encodeFunctionData(fnName: string, args: unknown[] = []): string {
return viemEncode({ abi: this.abi, functionName: fnName, args })
}
decodeFunctionData(fnName: string, data: string): Result {
const fn = this.abi.find((f) => f.type === 'function' && f.name === fnName)
if (!fn || !('outputs' in fn)) {
throw new Error(`Function ${fnName} not found in ABI`)
}
const decoded = decodeFunctionResult({
abi: this.abi,
functionName: fnName as any,
data: data as `0x${string}`,
})
// Viem doesn't return {result}, it returns the result directly
const values = decoded as any[]
const keys = fn.outputs?.map((o) => o.name || '') ?? []
return this.toResult(values, keys)
}
private toResult(values: any[], keys: string[]): Result {
// Create a new object with both array-like and named properties
const resultObj: Record<string | number, any> = {}
// Add array indices
values.forEach((val, i) => {
resultObj[i] = val
})
// Add named properties
keys.forEach((key, i) => {
if (key) {
// Only add if key name exists
resultObj[key] = values[i]
}
})
// Add length property
resultObj.length = values.length
// Use Object.assign to cast to Result
return Object.assign(values.slice(), resultObj) as unknown as Result
}
}
Key changes:
- Removed the destructuring of
{ result }
fromdecodeFunctionResult
- Cast the return value directly to
any[]
- Use that as your values array
Viem's decodeFunctionResult
returns the decoded value directly, not an object with a result
property.
lamlv2305: However, Jest was able to find: './react.d.ts'
You might want to include a file extension in your import, or update your 'moduleFileExtensions', which is currently ['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx', 'json', 'node'].
See https://jestjs.io/docs/configuration#modulefileextensions-arraystring
> 1 | 'use strict';var ethers=require('ethers'),asyncMutex=require('async-mutex'),idb=require('idb'),minikitJs=require('@worldcoin/minikit-js'),D=require('bignumber.js');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var D__default=/*#__PURE__*/_interopDefault(D);var L=Object.defineProperty;var F=(l,e)=>{for(var t in e)L(l,t,{get:e[t],enumerable:true});};var I={};F(I,{DEX_ABI:()=>f,MULTICALL3_ABI:()=>R,erc20:()=>d,uniswapQuoterV2ABI:()=>N,uniswapRouterV2ABI:()=>W,uniswapRouterV3ABI:()=>Q,viewQuoterv3ABI:()=>X});var f=[{type:"receive",stateMutability:"payable"},{type:"function",name:"fillQuoteEthToToken",inputs:[{name:"buyTokenAddress",type:"address",internalType:"address"},{name:"target",type:"address",internalType:"address payable"},{name:"swapCallData",type:"bytes",internalType:"bytes"},{name:"f
when i run jest, and fail, they log whole my library source code, how to prevent it /
GitHub Copilot: The issue is that Jest is failing to import modules and then showing your bundled source code in the error. To prevent Jest from showing your source code when tests fail, you can configure Jest to provide cleaner error messages.
Here's how to solve the issues:
- Fix the immediate import error
Jest can't find the modules because you need to update your jest.config.js
file.
module.exports = {
// Transform TypeScript files
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
// Tell Jest to look for test files with these extensions
moduleFileExtensions: ['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx', 'json', 'node'],
// Prevent verbose error reporting
verbose: false,
// Customize test reporters to reduce output
reporters: [
'default',
['jest-silent-reporter', { useDots: true, showWarnings: true }]
],
// Limit stack trace detail
testEnvironment: 'node',
// Reduce console output
silent: false,
// Don't print full error details
noStackTrace: true,
// Don't output full source when tests fail
testFailureExitCode: 1
};
- Install the silent reporter
yarn add --dev jest-silent-reporter
- Fix the import in your test file
Make sure file extensions match exactly and the file exists. For example, check if mutilcall3.ts
should be multicall3.ts
:
// Fix import path if needed
import { ViemMulticall3 } from './multicall3'; // or './mutilcall3' if that's correct
- Run Jest with minimal output
yarn jest --silent
These changes should help reduce the verbose error output and prevent your entire source code from being displayed when tests fail.
lamlv2305: ● Test suite failed to run
Cannot find module 'react' from 'node_modules/zustand/index.js'
Require stack:
node_modules/zustand/index.js
node_modules/@worldcoin/idkit-core/build/index.cjs
node_modules/@worldcoin/minikit-js/build/index.cjs
packages/core/dist/index.js
packages/viem/src/multicall3.test.ts
However, Jest was able to find:
'./react.d.ts'
You might want to include a file extension in your import, or update your 'moduleFileExtensions', which is currently ['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx', 'json', 'node'].
See https://jestjs.io/docs/configuration#modulefileextensions-arraystring
why i got that error
GitHub Copilot: The error shows that Jest is trying to load React when running your tests, even though your test shouldn't need React. This is happening because of dependencies in your import chain:
multicall3.test.ts → core/dist/index.js → @worldcoin/minikit-js → @worldcoin/idkit-core → zustand → react
When Jest tries to resolve the React module in zustand, it can't find it because:
- React is not installed in your project
- The test environment is set up for Node.js, not for React
Here's how to fix it:
Install React as a dev dependency
yarn add --dev react react-dom
Create a mock for React in your Jest config
// filepath: /Volumes/hs/work/worldchain-sdk/jest.config.js module.exports = { // ... other config moduleNameMapper: { '^react$': '<rootDir>/node_modules/react', '^react-dom$': '<rootDir>/node_modules/react-dom' }, // For Node.js environment testEnvironment: 'node', }
Alternatively, mock the dependencies
// filepath: /Volumes/hs/work/worldchain-sdk/jest.config.js module.exports = { // ... other config moduleNameMapper: { '@worldcoin/minikit-js': '<rootDir>/__mocks__/minikit-js.js', } }
Create the mock file
// filepath: /Volumes/hs/work/worldchain-sdk/__mocks__/minikit-js.js // Mock only what's needed by your tests module.exports = {}
Choose the approach that makes the most sense for your project. Option 1 adds React, while Option 3+4 mocks the dependency that's causing the issue.
lamlv2305: TypeError: abi.filter is not a function
GitHub Copilot: This error suggests that in your code, something is expecting abi
to be an array (so it can call filter
on it), but it's actually another type.
The issue is likely in your ViemMulticall3
class when you're using parseAbi(abi.MULTICALL3_ABI)
.
Let's fix it:
import { Aggregate, Aggregate3, Multicall3, abi, config } from '@holdstation/worldchain-sdk'
import { parseAbi, type PublicClient } from 'viem'
export class ViemMulticall3 implements Multicall3 {
private readonly address: `0x${string}`
private readonly parsedAbi: any
constructor(private publicClient: PublicClient) {
this.address = config.multicall3Address as `0x${string}`
// Check what type is abi.MULTICALL3_ABI
if (Array.isArray(abi.MULTICALL3_ABI)) {
// It's already an array, so use it directly
this.parsedAbi = abi.MULTICALL3_ABI
} else {
// It's a string or other format, so parse it
this.parsedAbi = parseAbi(abi.MULTICALL3_ABI)
}
}
async aggregate(calls: Aggregate['request'][]): Promise<Aggregate['response']> {
const result = await this.publicClient.readContract({
address: this.address,
abi: this.parsedAbi,
functionName: 'aggregate',
args: [calls],
})
// Cast the result to the expected tuple type
const resultArray = result as [bigint, { returnData: string }[]]
const blockNumber = Number(resultArray[0])
const returnStructs = resultArray[1]
return [blockNumber, returnStructs.map((c) => c.returnData)]
}
async aggregate3(calls: Aggregate3["request"][]): Promise<Aggregate3["response"][]> {
const results = await this.publicClient.readContract({
address: this.address,
abi: this.parsedAbi,
functionName: 'aggregate3',
args: [calls],
}) as { success: boolean; returnData: string }[]
return results.map((item) => ({
blockNumber: 0n, // No block number from aggregate3
returnData: item.returnData,
success: item.success,
error: item.success ? undefined : "Call failed"
}));
}
}
Key changes:
- Parse the ABI only once in the constructor
- Check if
abi.MULTICALL3_ABI
is already an array before parsing - Use the parsed ABI in both methods
This should fix the "abi.filter is not a function" error.
lamlv2305: i made mistake, i want to function similar with decodeFunctionResult
of ethers@5.8.0, not decodeFunctionData
GitHub Copilot: You're right - the function you implemented as decodeFunctionData
should actually be named decodeFunctionResult
to match ethers.js v5.8.0 naming.
Here's the correct implementation:
import { AbiCodec, Result } from '@holdstation/worldchain-sdk'
import { Abi, decodeFunctionResult as viemDecodeFunctionResult, encodeFunctionData as viemEncode } from 'viem'
export class ViemCodec implements AbiCodec {
constructor(private abi: Abi) {}
encodeFunctionData(fnName: string, args: unknown[] = []): string {
return viemEncode({ abi: this.abi, functionName: fnName, args })
}
decodeFunctionResult(fnName: string, data: string): Result {
const fn = this.abi.find((f) => f.type === 'function' && f.name === fnName)
if (!fn || !('outputs' in fn)) {
throw new Error(`Function ${fnName} not found in ABI`)
}
const decoded = viemDecodeFunctionResult({
abi: this.abi,
functionName: fnName as any,
data: data as `0x${string}`,
})
// Viem doesn't return {result}, it returns the result directly
const values = decoded as any[]
const keys = fn.outputs?.map((o) => o.name || '') ?? []
return this.toResult(values, keys)
}
private toResult(values: any[], keys: string[]): Result {
// Create a new object with both array-like and named properties
const resultObj: Record<string | number, any> = {}
// Add array indices
values.forEach((val, i) => {
resultObj[i] = val
})
// Add named properties
keys.forEach((key, i) => {
if (key) {
// Only add if key name exists
resultObj[key] = values[i]
}
})
// Add length property
resultObj.length = values.length
// Use Object.assign to cast to Result
return Object.assign(values.slice(), resultObj) as unknown as Result
}
}
Key changes:
- Changed the method name from
decodeFunctionData
todecodeFunctionResult
- Renamed the imported function from viem to
viemDecodeFunctionResult
for clarity - Kept the implementation logic the same since it was already using
decodeFunctionResult
from viem
This matches ethers.js v5.8.0's interface more closely, where:
encodeFunctionData
encodes function calls with argumentsdecodeFunctionResult
decodes the returned data from function calls
lamlv2305: update my publish.yml to work with new yarn workspaces.
I want to build whole packages. If any fail, please stop, after that, publish these packages to npm
GitHub Copilot: Here's an updated GitHub workflow for publishing your Yarn workspace packages:
name: Publish to npm
on:
push:
tags:
- 'v*.*.*' # Trigger only on version tags like v1.2.3
jobs:
publish:
name: Publish npm packages
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '20' # Using LTS version
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build packages
run: yarn workspaces run build
# This will build all packages in the workspace in the correct dependency order
# If any package build fails, the whole workflow will fail
- name: Publish packages
run: |
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
# Loop through each package in packages directory
for dir in packages/*; do
if [ -f "$dir/package.json" ]; then
echo "Publishing $dir"
cd $dir
# Check if the package is private
IS_PRIVATE=$(node -p "require('./package.json').private")
if [ "$IS_PRIVATE" != "true" ]; then
npm publish --access public
echo "✅ Published $dir"
else
echo "⏭️ Skipping private package $dir"
fi
cd ../../
fi
done
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Key improvements:
Uses Node.js LTS (v20) instead of the unstable Node 23
Builds all workspace packages in dependency order using
yarn workspaces run build
- If any package build fails, the workflow will stop
Intelligently publishes all non-private packages by:
- Looping through each package directory
- Checking if the package is private
- Publishing only non-private packages
- Using
--access public
for scoped packages
Better organization with separate steps for building and publishing
This workflow ensures all your packages are built together, dependencies are respected, and only publishable packages are sent to npm.
lamlv2305: hexZeroPad for ethers@v6
GitHub Copilot: The hexZeroPad
method in your EthersClient
needs to be updated for ethers.js v6. In v6, the utility functions have been reorganized, and hexZeroPad
is now accessed differently than in v5.
Here's the updated implementation for ethers.js v6:
import { AbiCodec, Client, FilterLogs, logger, OnchainTransaction, Result } from "@holdstation/worldchain-sdk";
import { ethers } from "ethers";
import { EthersCodec } from "./codec";
export class EthersClient implements Client {
// ...existing code...
hexZeroPad(value: string, length: number): string {
// In ethers v6, hexZeroPad is now in the ethers.zeroPadValue function
// The signature has changed to take a BytesLike and return a "0x"-prefixed string
let hexValue = value;
// Make sure the value has '0x' prefix
if (!hexValue.startsWith('0x')) {
hexValue = '0x' + hexValue;
}
return ethers.zeroPadValue(hexValue, length);
}
// ...rest of the class...
}
Key changes:
ethers.hexZeroPad
has been replaced withethers.zeroPadValue
- The method now ensures that the value is properly prefixed with '0x'
- The function still accepts the same parameters (value and length)
This updated version is compatible with ethers.js v6 while maintaining the same functionality.
lamlv2305: i have that script to bump version of package.json, however, they have devDependencies and peerDependencies still need to update to align with each other.
Modify it please
GitHub Copilot: Here's an improved version of your bump script that also updates internal dependencies and peer dependencies to keep versions aligned:
const fs = require("fs");
const path = require("path");
const bump = (version, type) => {
const [major, minor, patch] = version.split(".").map(Number);
if (type === "major") return `${major + 1}.0.0`;
if (type === "minor") return `${major}.${minor + 1}.0`;
return `${major}.${minor}.${patch + 1}`;
};
const packagesDir = path.join(__dirname, "../packages");
const type = process.argv[2] || "patch";
// Step 1: Read all package.json files and their current versions
const packages = {};
fs.readdirSync(packagesDir).forEach((pkgName) => {
const pkgPath = path.join(packagesDir, pkgName);
const stat = fs.statSync(pkgPath);
// Skip non-directories and hidden files
if (!stat.isDirectory() || pkgName.startsWith(".")) return;
const pkgJsonPath = path.join(pkgPath, "package.json");
if (!fs.existsSync(pkgJsonPath)) return;
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
packages[pkg.name] = {
path: pkgJsonPath,
currentVersion: pkg.version,
newVersion: bump(pkg.version, type),
json: pkg
};
});
// Step 2: Update versions in each package.json
Object.keys(packages).forEach((packageName) => {
const pkg = packages[packageName];
const pkgJson = pkg.json;
// Update the package's own version
pkgJson.version = pkg.newVersion;
// Update dependencies that reference other packages in our workspace
const dependencyTypes = ["dependencies", "devDependencies", "peerDependencies"];
dependencyTypes.forEach(depType => {
if (!pkgJson[depType]) return;
Object.keys(pkgJson[depType]).forEach(dependency => {
// If this is one of our packages, update its version
if (packages[dependency]) {
pkgJson[depType][dependency] = `^${packages[dependency].newVersion}`;
}
});
});
// Write updated package.json back to file
fs.writeFileSync(pkg.path, JSON.stringify(pkgJson, null, 2) + "\n");
console.log(`✅ Bumped ${packageName} from ${pkg.currentVersion} to ${pkg.newVersion}`);
});
// Log a summary
console.log("\n📦 Summary of version bumps:");
Object.keys(packages).forEach(name => {
console.log(` ${name}: ${packages[name].currentVersion} → ${packages[name].newVersion}`);
});
Key improvements:
Two-step process:
- First reads all package information
- Then updates all versions with knowledge of all new versions
Updates dependencies:
- Checks
dependencies
,devDependencies
, andpeerDependencies
- Updates any references to internal packages with their new versions
- Checks
Better output formatting:
- Shows the version change from old to new
- Adds a summary at the end of all packages that were bumped
- Preserves newlines at end of package.json files
More maintainable:
- Organizes data in a packages object for easier referencing
- Reuses code for different dependency types
This script maintains your original functionality while ensuring all internal package references stay in sync during version bumps.
lamlv2305: if (!fs.existsSync(pkgJsonPath)) return;
update styles please, never use inline return please
GitHub Copilot: Here's the updated bump script with improved coding style, avoiding inline returns and with better formatting:
const fs = require("fs");
const path = require("path");
/**
* Bumps a semantic version based on type (major, minor, patch)
* @param {string} version - Current version in format "x.y.z"
* @param {string} type - "major", "minor", or "patch"
* @returns {string} Bumped version
*/
const bump = (version, type) => {
const [major, minor, patch] = version.split(".").map(Number);
if (type === "major") {
return `${major + 1}.0.0`;
}
if (type === "minor") {
return `${major}.${minor + 1}.0`;
}
return `${major}.${minor}.${patch + 1}`;
};
// Step 1: Read all package.json files and their current versions
const packagesDir = path.join(__dirname, "../packages");
const type = process.argv[2] || "patch";
const packages = {};
fs.readdirSync(packagesDir).forEach((pkgName) => {
const pkgPath = path.join(packagesDir, pkgName);
const stat = fs.statSync(pkgPath);
// Skip non-directories and hidden files
if (!stat.isDirectory() || pkgName.startsWith(".")) {
return;
}
const pkgJsonPath = path.join(pkgPath, "package.json");
if (!fs.existsSync(pkgJsonPath)) {
return;
}
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
packages[pkg.name] = {
path: pkgJsonPath,
currentVersion: pkg.version,
newVersion: bump(pkg.version, type),
json: pkg
};
});
// Step 2: Update versions in each package.json
Object.keys(packages).forEach((packageName) => {
const pkg = packages[packageName];
const pkgJson = pkg.json;
// Update the package's own version
pkgJson.version = pkg.newVersion;
// Update dependencies that reference other packages in our workspace
const dependencyTypes = ["dependencies", "devDependencies", "peerDependencies"];
dependencyTypes.forEach(depType => {
if (!pkgJson[depType]) {
return;
}
Object.keys(pkgJson[depType]).forEach(dependency => {
// If this is one of our packages, update its version
if (packages[dependency]) {
pkgJson[depType][dependency] = `^${packages[dependency].newVersion}`;
}
});
});
// Write updated package.json back to file
fs.writeFileSync(pkg.path, JSON.stringify(pkgJson, null, 2) + "\n");
console.log(`✅ Bumped ${packageName} from ${pkg.currentVersion} to ${pkg.newVersion}`);
});
// Log a summary
console.log("\n📦 Summary of version bumps:");
Object.keys(packages).forEach(name => {
console.log(` ${name}: ${packages[name].currentVersion} → ${packages[name].newVersion}`);
});
Style improvements:
- Used block-style returns with curly braces instead of inline returns
- Added JSDoc comments for the bump function
- Split the process into clear steps with comments
- Added newlines for better code readability
- Consistent spacing and formatting
- Added explicit returns for all conditional branches
- Preserved the functionality of updating dependent package versions
The code now follows a more consistent style while maintaining the same functionality.
lamlv2305: give me test suite example for monorepo.
the test case for #folder:ether-v5 #folder:ether-v6 #folder:viem must be the same, they are the diffirent intergrate with third party, so input and output of them must be exactly the same cuz we return raw js object
GitHub Copilot: # Test Suite Example for Worldchain SDK Monorepo
Here's a comprehensive test suite structure for your monorepo that ensures consistent behavior across all packages (ethers-v5
, ethers-v6
, and viem
).
Shared Test Suite Structure
First, let's create a shared test folder with common test cases:
import { Client, Multicall3, AbiCodec, TokenProvider } from '@holdstation/worldchain-sdk'
export interface TestProvider {
name: string;
getClient: () => Client;
getMulticall3: () => Multicall3;
getAbiCodec: (abi: any) => AbiCodec;
}
export const ERC20_ABI = [
'function balanceOf(address owner) view returns (uint256)',
'function decimals() view returns (uint8)',
'function symbol() view returns (string)',
'function name() view returns (string)',
]
export const TEST_ADDRESS = '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1' // Some token
export const TEST_USER_ADDRESS = '0xEd10C200aFc35AF91A45E8BE53cd5a299F93F32F' // Some user address
export function runTests(provider: TestProvider) {
describe(`${provider.name} integration tests`, () => {
let client: Client
let multicall3: Multicall3
let codec: AbiCodec
beforeAll(() => {
client = provider.getClient()
multicall3 = provider.getMulticall3()
codec = provider.getAbiCodec(ERC20_ABI)
})
describe('Client', () => {
test('should report chain id', async () => {
const chainId = client.getChainId()
expect(chainId).toBeGreaterThan(0)
})
test('should check address validity', () => {
expect(client.isValidAddress(TEST_ADDRESS)).toBe(true)
expect(client.isValidAddress('0xinvalid')).toBe(false)
})
test('should get block number', async () => {
const blockNumber = await client.getBlockNumber()
expect(blockNumber).toBeGreaterThan(0)
})
test('should pad hex values', () => {
const padded = client.hexZeroPad('0x1', 32)
expect(padded.length).toBe(66) // 0x + 64 characters
expect(padded.startsWith('0x')).toBe(true)
})
})
describe('AbiCodec', () => {
test('should encode function data', () => {
const data = codec.encodeFunctionData('balanceOf', [TEST_USER_ADDRESS])
expect(data).toMatch(/^0x/)
})
test('should decode function result', () => {
// Mock result from balanceOf call
const mockData = '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000' // 1 ETH
const result = codec.decodeFunctionResult('balanceOf', mockData)
// All implementations should return the same structure
expect(result.length).toBe(1)
expect(result[0].toString()).toBe('1000000000000000000')
})
})
describe('Multicall3', () => {
test('should perform aggregate call', async () => {
const calls = [
{
target: TEST_ADDRESS,
callData: codec.encodeFunctionData('balanceOf', [TEST_USER_ADDRESS])
},
{
target: TEST_ADDRESS,
callData: codec.encodeFunctionData('decimals', [])
}
]
const result = await multicall3.aggregate(calls)
expect(Array.isArray(result)).toBe(true)
expect(result.length).toBe(2)
expect(result[0]).toBeGreaterThan(0) // Block number
expect(result[1].length).toBe(2) // Two results
})
test('should perform aggregate3 call', async () => {
const calls = [
{
target: TEST_ADDRESS,
callData: codec.encodeFunctionData('balanceOf', [TEST_USER_ADDRESS]),
allowFailure: true
},
{
target: TEST_ADDRESS,
callData: codec.encodeFunctionData('decimals', []),
allowFailure: true
}
]
const result = await multicall3.aggregate3(calls)
expect(Array.isArray(result)).toBe(true)
expect(result.length).toBe(2)
expect(result[0].success).toBe(true)
expect(result[1].success).toBe(true)
})
})
describe('TokenProvider', () => {
let tokenProvider: TokenProvider
beforeAll(() => {
tokenProvider = new TokenProvider({ client, multicall3 })
})
test('should fetch token details', async () => {
const tokenInfo = await tokenProvider.details(TEST_ADDRESS)
expect(tokenInfo.address).toBe(TEST_ADDRESS.toLowerCase())
expect(tokenInfo.name).toBeTruthy()
expect(tokenInfo.symbol).toBeTruthy()
expect(tokenInfo.decimals).toBeGreaterThan(0)
})
})
})
}
Implementation for Each Package
Now, create a test file for each package:
Ethers v5 Tests
import { ethers } from 'ethers'
import { EthersClient } from '../src/client'
import { EthersCodec } from '../src/codec'
import { EthersMulticall3 } from '../src/multicall3'
import { runTests, TestProvider, ERC20_ABI } from '../../../tests/shared'
const RPC_URL = 'https://worldchain-mainnet.g.alchemy.com/public'
const provider: TestProvider = {
name: 'ethers-v5',
getClient: () => {
const ethersProvider = new ethers.providers.JsonRpcProvider(RPC_URL)
return new EthersClient(ethersProvider)
},
getMulticall3: () => {
const ethersProvider = new ethers.providers.JsonRpcProvider(RPC_URL)
return new EthersMulticall3(ethersProvider)
},
getAbiCodec: (abi) => {
return new EthersCodec(abi)
}
}
runTests(provider)
Ethers v6 Tests
import { ethers } from 'ethers'
import { EthersClient } from '../src/client'
import { EthersCodec } from '../src/codec'
import { EthersMulticall3 } from '../src/multicall3'
import { runTests, TestProvider, ERC20_ABI } from '../../../tests/shared'
const RPC_URL = 'https://worldchain-mainnet.g.alchemy.com/public'
const provider: TestProvider = {
name: 'ethers-v6',
getClient: () => {
const ethersProvider = new ethers.JsonRpcProvider(RPC_URL)
return new EthersClient(ethersProvider)
},
getMulticall3: () => {
const ethersProvider = new ethers.JsonRpcProvider(RPC_URL)
return new EthersMulticall3(ethersProvider)
},
getAbiCodec: (abi) => {
return new EthersCodec(abi)
}
}
runTests(provider)
Viem Tests
import { createPublicClient, http } from 'viem'
import { worldchain } from 'viem/chains'
import { ViemClient } from '../src/client'
import { ViemCodec } from '../src/codec'
import { ViemMulticall3 } from '../src/mutilcall3'
import { runTests, TestProvider, ERC20_ABI } from '../../../tests/shared'
const RPC_URL = 'https://worldchain-mainnet.g.alchemy.com/public'
const provider: TestProvider = {
name: 'viem',
getClient: () => {
const publicClient = createPublicClient({
chain: worldchain,
transport: http(RPC_URL),
batch: { multicall: true }
})
return new ViemClient(publicClient)
},
getMulticall3: () => {
const publicClient = createPublicClient({
chain: worldchain,
transport: http(RPC_URL),
batch: { multicall: true }
})
return new ViemMulticall3(publicClient)
},
getAbiCodec: (abi) => {
return new ViemCodec(abi)
}
}
runTests(provider)
Jest Configuration
Create a Jest config to ensure tests run correctly:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/tests/**/*.test.ts'],
transform: {
'^.+\\.tsx?$': ['ts-jest', {
tsconfig: './tsconfig.json',
isolatedModules: true
}]
},
moduleNameMapper: {
'^react$': '<rootDir>/node_modules/react',
'^react-dom$': '<rootDir>/node_modules/react-dom'
},
testTimeout: 30000, // Increased timeout for blockchain calls
setupFilesAfterEnv: ['./jest.setup.js']
}
// Silence console.log during tests
console.log = jest.fn();
// But keep errors
console.error = console.error;
Running Tests
Add these scripts to your root package.json:
{
"scripts": {
"test": "jest",
"test:ethers-v5": "jest packages/ethers-v5",
"test:ethers-v6": "jest packages/ethers-v6",
"test:viem": "jest packages/viem"
}
}
This structure ensures:
- All implementations are tested against the same test cases
- All packages must return the same data structures
- Tests are run against a real blockchain for integration testing
- You can test individual packages or all at once
lamlv2305: can i split describe
to multiple files
GitHub Copilot: Yes, you can split your describe
blocks into multiple files for better organization and maintainability. This approach is especially useful for large test suites in a monorepo.
Here's how you can restructure the tests:
1. Create separate test files for each component
import { Client } from '@holdstation/worldchain-sdk'
import { TestProvider, TEST_ADDRESS } from './common'
export function runClientTests(provider: TestProvider) {
describe(`${provider.name} Client tests`, () => {
let client: Client
beforeAll(() => {
client = provider.getClient()
})
test('should report chain id', () => {
const chainId = client.getChainId()
expect(chainId).toBeGreaterThan(0)
})
test('should check address validity', () => {
expect(client.isValidAddress(TEST_ADDRESS)).toBe(true)
expect(client.isValidAddress('0xinvalid')).toBe(false)
})
test('should get block number', async () => {
const blockNumber = await client.getBlockNumber()
expect(blockNumber).toBeGreaterThan(0)
})
test('should pad hex values', () => {
const padded = client.hexZeroPad('0x1', 32)
expect(padded.length).toBe(66) // 0x + 64 characters
expect(padded.startsWith('0x')).toBe(true)
})
})
}
import { AbiCodec } from '@holdstation/worldchain-sdk'
import { TestProvider, ERC20_ABI, TEST_USER_ADDRESS } from './common'
export function runCodecTests(provider: TestProvider) {
describe(`${provider.name} AbiCodec tests`, () => {
let codec: AbiCodec
beforeAll(() => {
codec = provider.getAbiCodec(ERC20_ABI)
})
test('should encode function data', () => {
const data = codec.encodeFunctionData('balanceOf', [TEST_USER_ADDRESS])
expect(data).toMatch(/^0x/)
})
test('should decode function result', () => {
// Mock result from balanceOf call
const mockData = '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000' // 1 ETH
const result = codec.decodeFunctionResult('balanceOf', mockData)
// All implementations should return the same structure
expect(result.length).toBe(1)
expect(result[0].toString()).toBe('1000000000000000000')
})
})
}
import { Multicall3, AbiCodec } from '@holdstation/worldchain-sdk'
import { TestProvider, ERC20_ABI, TEST_ADDRESS, TEST_USER_ADDRESS } from './common'
export function runMulticall3Tests(provider: TestProvider) {
describe(`${provider.name} Multicall3 tests`, () => {
let multicall3: Multicall3
let codec: AbiCodec
beforeAll(() => {
multicall3 = provider.getMulticall3()
codec = provider.getAbiCodec(ERC20_ABI)
})
test('should perform aggregate call', async () => {
const calls = [
{
target: TEST_ADDRESS,
callData: codec.encodeFunctionData('balanceOf', [TEST_USER_ADDRESS])
},
{
target: TEST_ADDRESS,
callData: codec.encodeFunctionData('decimals', [])
}
]
const result = await multicall3.aggregate(calls)
expect(Array.isArray(result)).toBe(true)
expect(result.length).toBe(2)
expect(result[0]).toBeGreaterThan(0) // Block number
expect(result[1].length).toBe(2) // Two results
})
test('should perform aggregate3 call', async () => {
const calls = [
{
target: TEST_ADDRESS,
callData: codec.encodeFunctionData('balanceOf', [TEST_USER_ADDRESS]),
allowFailure: true
},
{
target: TEST_ADDRESS,
callData: codec.encodeFunctionData('decimals', []),
allowFailure: true
}
]
const result = await multicall3.aggregate3(calls)
expect(Array.isArray(result)).toBe(true)
expect(result.length).toBe(2)
expect(result[0].success).toBe(true)
expect(result[1].success).toBe(true)
})
})
}
2. Create a common file for shared constants and interfaces
import { Client, Multicall3, AbiCodec } from '@holdstation/worldchain-sdk'
export interface TestProvider {
name: string;
getClient: () => Client;
getMulticall3: () => Multicall3;
getAbiCodec: (abi: any) => AbiCodec;
}
export const ERC20_ABI = [
'function balanceOf(address owner) view returns (uint256)',
'function decimals() view returns (uint8)',
'function symbol// filepath: /Volumes/hs/work/worldchain-sdk/tests/shared/common.ts
import { Client, Multicall3, AbiCodec } from '@holdstation/worldchain-sdk'
export interface TestProvider {
name: string;
getClient: () => Client;
getMulticall3: () => Multicall3;
getAbiCodec: (abi: any) => AbiCodec;
}
export const ERC20_ABI = [
'function balanceOf(address owner) view returns (uint256)',
'function decimals() view returns (uint8)',
'function symbol
lamlv2305: can i split `describe` to multiple files
GitHub Copilot: Yes, you can definitely split your `describe` blocks into multiple files to make your test suite more modular and maintainable. Here's how to structure it:
## 1. Split the Shared Tests by Feature
Create separate test files for each feature:
````typescript
import { Client } from '@holdstation/worldchain-sdk'
import { TestProvider, TEST_ADDRESS } from './setup'
export function runClientTests(provider: TestProvider) {
describe(`${provider.name}: Client`, () => {
let client: Client
beforeAll(() => {
client = provider.getClient()
})
test('should report chain id', async () => {
const chainId = client.getChainId()
expect(chainId).toBeGreaterThan(0)
})
test('should check address validity', () => {
expect(client.isValidAddress(TEST_ADDRESS)).toBe(true)
expect(client.isValidAddress('0xinvalid')).toBe(false)
})
test('should get block number', async () => {
const blockNumber = await client.getBlockNumber()
expect(blockNumber).toBeGreaterThan(0)
})
test('should pad hex values', () => {
const padded = client.hexZeroPad('0x1', 32)
expect(padded.length).toBe(66) // 0x + 64 characters
expect(padded.startsWith('0x')).toBe(true)
})
})
}
import { AbiCodec } from '@holdstation/worldchain-sdk'
import { TestProvider, TEST_USER_ADDRESS, ERC20_ABI } from './setup'
export function runCodecTests(provider: TestProvider) {
describe(`${provider.name}: AbiCodec`, () => {
let codec: AbiCodec
beforeAll(() => {
codec = provider.getAbiCodec(ERC20_ABI)
})
test('should encode function data', () => {
const data = codec.encodeFunctionData('balanceOf', [TEST_USER_ADDRESS])
expect(data).toMatch(/^0x/)
})
test('should decode function result', () => {
// Mock result from balanceOf call
const mockData = '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000' // 1 ETH
const result = codec.decodeFunctionResult('balanceOf', mockData)
// All implementations should return the same structure
expect(result.length).toBe(1)
expect(result[0].toString()).toBe('1000000000000000000')
})
})
}
import { Multicall3, AbiCodec } from '@holdstation/worldchain-sdk'
import { TestProvider, TEST_ADDRESS, TEST_USER_ADDRESS, ERC20_ABI } from './setup'
export function runMulticallTests(provider: TestProvider) {
describe(`${provider.name}: Multicall3`, () => {
let multicall3: Multicall3
let codec: AbiCodec
beforeAll(() => {
multicall3 = provider.getMulticall3()
codec = provider.getAbiCodec(ERC20_ABI)
})
test('should perform aggregate call', async () => {
const calls = [
{
target: TEST_ADDRESS,
callData: codec.encodeFunctionData('balanceOf', [TEST_USER_ADDRESS])
},
{
target: TEST_ADDRESS,
callData: codec.encodeFunctionData('decimals', [])
}
]
const result = await multicall3.aggregate(calls)
expect(Array.isArray(result)).toBe(true)
expect(result.length).toBe(2)
expect(result[0]).toBeGreaterThan(0) // Block number
expect(result[1].length).toBe(2) // Two results
})
test('should perform aggregate3 call', async () => {
const calls = [
{
target: TEST_ADDRESS,
callData: codec.encodeFunctionData('balanceOf', [TEST_USER_ADDRESS]),
allowFailure: true
},
{
target: TEST_ADDRESS,
callData: codec.encodeFunctionData('decimals', []),
allowFailure: true
}
]
const result = await multicall3.aggregate3(calls)
expect(Array.isArray(result)).toBe(true)
expect(result.length).toBe(2)
expect(result[0].success).toBe(true)
expect(result[1].success).toBe(true)
})
})
}
2. Create a Common Setup File
import { Client, Multicall3, AbiCodec } from '@holdstation/worldchain-sdk'
export interface TestProvider {
name: string;
getClient: () => Client;
getMulticall3: () => Multicall3;
getAbiCodec: (abi: any) => AbiCodec;
}
export const ERC20_ABI = [
'function balanceOf(address owner) view returns (uint256)',
'function decimals() view returns (uint8)',
'function symbol() view returns (string)',
'function name() view returns (string)',
]
export const TEST_ADDRESS = '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1' // Some token
export const TEST_USER_ADDRESS = '0xEd10C200aFc35AF91A45E8BE53cd5a299F93F32F' // Some user address
3. Create an Integration File
import { TestProvider } from './setup'
import { runClientTests } from './client.test'
import { runCodecTests } from './codec.test'
import { runMulticallTests } from './multicall3.test'
export function runAllTests(provider: TestProvider) {
describe(`${provider.name} integration tests`, () => {
runClientTests(provider)
runCodecTests(provider)
runMulticallTests(provider)
})
}
export * from './setup'
4. Use in Individual Packages
import { ethers } from 'ethers'
import { EthersClient } from '../src/client'
import { EthersCodec } from '../src/codec'
import { EthersMulticall3 } from '../src/multicall3'
import { runAllTests, TestProvider, ERC20_ABI } from '../../../tests/shared'
const RPC_URL = 'https://worldchain-mainnet.g.alchemy.com/public'
const provider: TestProvider = {
name: 'ethers-v6',
getClient: () => {
const ethersProvider = new ethers.JsonRpcProvider(RPC_URL)
return new EthersClient(ethersProvider)
},
getMulticall3: () => {
const ethersProvider = new ethers.JsonRpcProvider(RPC_URL)
return new EthersMulticall3(ethersProvider)
},
getAbiCodec: (abi) => {
return new EthersCodec(abi)
}
}
runAllTests(provider)
Benefits of This Approach
- Modularity: Each feature has its own test file
- Selective Testing: You can choose to run only specific test suites
- Maintainability: Easier to add/modify tests for specific features
- Clarity: Tests are organized by feature rather than by implementation
- Reuse: The same test cases run across all implementations
This approach still ensures all implementations are tested against the same test cases, but with much better organization.
lamlv2305: give me tree of folder structure to implement these tests.
I want to have a separate folder <root>/tests
for whole integrate test suite. Then another package can easily re-use it
GitHub Copilot:
lamlv2305: give me tree of folder structure to implement these tests.
I want to have a separate folder <root>/tests
for whole integrate test suite. Then another package can easily re-use it
GitHub Copilot: # Monorepo Test Structure
Here's a recommended folder structure for implementing the shared test suite in your monorepo:
/worldchain-sdk/
├── .github/
│ └── workflows/
│ └── publish.yml
├── package.json (root workspace)
├── tsconfig.json (root config)
├── jest.config.js
├── jest.setup.js
│
├── packages/
│ ├── core/
│ │ ├── src/
│ │ ├── dist/
│ │ └── package.json
│ │
│ ├── ethers-v5/
│ │ ├── src/
│ │ ├── tests/
│ │ │ └── integration.test.ts (imports from /tests/shared)
│ │ ├── dist/
│ │ └── package.json
│ │
│ ├── ethers-v6/
│ │ ├── src/
│ │ ├── tests/
│ │ │ └── integration.test.ts (imports from /tests/shared)
│ │ ├── dist/
│ │ └── package.json
│ │
│ └── viem/
│ ├── src/
│ ├── tests/
│ │ └── integration.test.ts (imports from /tests/shared)
│ ├── dist/
│ └── package.json
│
└── tests/ (shared test suite)
├── shared/
│ ├── index.ts (main entry point)
│ ├── setup.ts (shared test config)
│ ├── client.test.ts
│ ├── codec.test.ts
│ ├── multicall3.test.ts
│ └── constants.ts (test addresses, ABIs, etc.)
│
├── helpers/
│ ├── rpc-provider.ts
│ └── mock-data.ts
│
└── fixtures/
└── abis/
├── erc20.ts
└── multicall3.ts
Key Components
Root tests Folder
- Contains all shared tests that can be imported by any package
- Completely package-independent
Shared Test Suite
/tests/shared/index.ts
- Main entry point exporting all test runners/tests/shared/setup.ts
- Common test setup code (interfaces, providers)- Individual test files for each feature
Package-Specific Tests
- Each package has its own tests folder
integration.test.ts
imports the shared test suite- Configures package-specific implementations as test providers
Test Helpers & Fixtures
- Common utilities, mock data, and test fixtures
- RPC providers, ABIs, and contract addresses
Implementation Details
1. Setting up imports in the root package.json
{
"devDependencies": {
"jest": "^29.7.0",
"ts-jest": "^29.1.1"
},
"scripts": {
"test": "jest",
"test:ethers-v5": "jest packages/ethers-v5",
"test:ethers-v6": "jest packages/ethers-v6",
"test:viem": "jest packages/viem"
}
}
2. Package integration test example
// packages/ethers-v6/tests/integration.test.ts
import { runAllTests } from '../../../tests/shared';
import { createEthersV6Provider } from '../../../tests/helpers/rpc-provider';
const provider = createEthersV6Provider();
runAllTests(provider);
3. Helper for RPC provider
// tests/helpers/rpc-provider.ts
import { ethers as ethersV5 } from 'ethers-v5';
import { ethers as ethersV6 } from 'ethers';
import { createPublicClient, http } from 'viem';
import { worldchain } from 'viem/chains';
import { TestProvider } from '../shared/setup';
import { EthersClient as EthersV5Client } from '../../packages/ethers-v5/src';
import { EthersClient as EthersV6Client } from '../../packages/ethers-v6/src';
import { ViemClient } from '../../packages/viem/src';
const RPC_URL = 'https://worldchain-mainnet.g.alchemy.com/public';
export function createEthersV5Provider(): TestProvider {
return {
name: 'ethers-v5',
getClient: () => {
const provider = new ethersV5.providers.JsonRpcProvider(RPC_URL);
return new EthersV5Client(provider);
},
// ... other implementations
};
}
// Similar functions for ethers-v6 and viem
This structure keeps your tests modular, reusable, and maintainable while ensuring consistent behavior across all implementations.
lamlv2305: Rewrite README.MD for me based on styles of https://github.com/dethcrypto/TypeChain/blob/master/README.md
GitHub Copilot: # Worldchain SDK
🔗 TypeScript-powered library for interacting with Worldchain blockchain with multiple provider options
Features
- 🛠 Multiple Provider Integration: Choose between ethers.js v5/v6 or viem
- 🔄 Unified API: Same interface regardless of the underlying provider
- 📦 Tree-shakable: Import only what you need
- 🔒 Type-safe: Full TypeScript support with complete typings
- 🚀 Multicall Support: Batch multiple calls for efficient interactions
- 🧩 Modular Design: Core functionality is provider-agnostic
Installation
```bash
Core package only
npm install @holdstation/worldchain-sdk
With ethers.js v5 support
npm in