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
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,19 @@
"types": "./dist/esm/crypto/index.d.ts",
"import": "./dist/esm/crypto/index.js",
"require": "./dist/cjs/crypto/index.js"
},
"./core": {
"types": "./dist/esm/core/index.d.ts",
"import": "./dist/esm/core/index.js",
"require": "./dist/cjs/core/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsc --skipLibCheck -p tsconfig.esm.json && tsc --skipLibCheck -p tsconfig.cjs.json && node -e \"fs.mkdirSync('dist/esm', {recursive: true}); fs.writeFileSync('dist/esm/package.json', '{\\\"type\\\": \\\"module\\\"}')\" && node -e \"fs.mkdirSync('dist/cjs', {recursive: true}); fs.writeFileSync('dist/cjs/package.json', '{\\\"type\\\": \\\"commonjs\\\"}')\"",
"typecheck": "tsc --skipLibCheck -p tsconfig.json --noEmit",
"build": "tsc -p tsconfig.esm.json && tsc -p tsconfig.cjs.json && node -e \"fs.mkdirSync('dist/esm', {recursive: true}); fs.writeFileSync('dist/esm/package.json', '{\\\"type\\\": \\\"module\\\"}')\" && node -e \"fs.mkdirSync('dist/cjs', {recursive: true}); fs.writeFileSync('dist/cjs/package.json', '{\\\"type\\\": \\\"commonjs\\\"}')\"",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "vitest run",
"bench": "node bench/signer.bench.mjs",
"docs": "typedoc"
Expand Down
15 changes: 10 additions & 5 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,55 +152,60 @@ export function createWhiteChainClient(config: WhiteChainConfig & { provider?: a
const wc = requireWallet()
const abi = requireGrantAbi()
return (wc as any).writeContract({
chain: config.chain as any,
address: addresses.grant,
abi,
functionName: 'submitApplication',
args: [grantId, applicant, metadataUri],
})
} as any)
},

async approveApplication({ applicationId }) {
const wc = requireWallet()
const abi = requireGrantAbi()
return (wc as any).writeContract({
chain: config.chain as any,
address: addresses.grant,
abi,
functionName: 'approveApplication',
args: [applicationId],
})
} as any)
},

async submitMilestoneEvidence({ milestoneId, evidenceUri }) {
const wc = requireWallet()
const abi = requireGrantAbi()
return (wc as any).writeContract({
chain: config.chain as any,
address: addresses.grant,
abi,
functionName: 'submitMilestoneEvidence',
args: [milestoneId, evidenceUri],
})
} as any)
},

async approveMilestone({ milestoneId }) {
const wc = requireWallet()
const abi = requireGrantAbi()
return (wc as any).writeContract({
chain: config.chain as any,
address: addresses.grant,
abi,
functionName: 'approveMilestone',
args: [milestoneId],
})
} as any)
},

async releasePayout({ milestoneId }) {
const wc = requireWallet()
const abi = requireGrantAbi()
return (wc as any).writeContract({
chain: config.chain as any,
address: addresses.grant,
abi,
functionName: 'releasePayout',
args: [milestoneId],
})
} as any)
},
submitApplication: withGasEstimation(
async ({ grantId, applicant, metadataUri }) => {
Expand Down
137 changes: 137 additions & 0 deletions src/core/Multicall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import type { Address, Hex, PublicClient } from 'viem'
import type { MulticallCall, MulticallResult, MulticallOptions } from '../types/multicall.js'
import { WhiteChainError } from '../types.js'

/**
* Standard Multicall3 contract address deployed on Whitechain and EVM networks.
*/
export const MULTICALL3_DEFAULT_ADDRESS: Address = '0xca11bde05977b3631167028862be2a173976ca11'

/**
* Minimal Multicall3 ABI for aggregate3 execution.
*/
export const MULTICALL3_ABI = [
{
inputs: [
{
components: [
{ name: 'target', type: 'address' },
{ name: 'allowFailure', type: 'bool' },
{ name: 'callData', type: 'bytes' },
],
name: 'calls',
type: 'tuple[]',
},
],
name: 'aggregate3',
outputs: [
{
components: [
{ name: 'success', type: 'bool' },
{ name: 'returnData', type: 'bytes' },
],
name: 'returnData',
type: 'tuple[]',
},
],
stateMutability: 'payable',
type: 'function',
},
] as const

export class Multicall {
public readonly multicallAddress: Address
private publicClient?: PublicClient

constructor(options?: { publicClient?: PublicClient; multicallAddress?: Address }) {
this.publicClient = options?.publicClient
this.multicallAddress = options?.multicallAddress ?? MULTICALL3_DEFAULT_ADDRESS
}

/**
* Batches multiple view calls into a single RPC eth_call request to Multicall3.
*
* @param calls Array of MulticallCall targets and callData payloads.
* @param options Execution overrides (publicClient, multicallAddress, default allowFailure).
* @returns Clean, typed array of MulticallResult matching the input calls.
*/
public async aggregate<TCalls extends readonly MulticallCall[]>(
calls: TCalls,
options?: MulticallOptions
): Promise<{ [K in keyof TCalls]: MulticallResult<TCalls[K] extends MulticallCall<infer R> ? R : any> }> {
if (!calls || calls.length === 0) {
return [] as any
}

const client = options?.publicClient ?? this.publicClient
if (!client) {
throw new WhiteChainError('No publicClient provided for Multicall aggregate execution')
}

const multicallAddress = options?.multicallAddress ?? this.multicallAddress

// Format calls into Multicall3 aggregate3 call tuples: [target, allowFailure, callData]
const formattedCalls = calls.map((c) => ({
target: c.target,
allowFailure: c.allowFailure ?? options?.allowFailure ?? true,
callData: c.callData,
}))

// Execute single eth_call to Multicall3 aggregate3
const rawResults = (await (client as any).readContract({
address: multicallAddress,
abi: MULTICALL3_ABI,
functionName: 'aggregate3',
args: [formattedCalls],
})) as Array<{ success: boolean; returnData: Hex }>

// Process and decode each return tuple
const results = rawResults.map((res, i) => {
const call = calls[i]
const success = res.success
const returnData = res.returnData

if (!success) {
return {
success: false,
data: null,
returnData,
error: new WhiteChainError(`Multicall view function reverted at index ${i} (target: ${call.target})`),
}
}

if (call.decoder) {
try {
const decodedData = call.decoder(returnData)
return {
success: true,
data: decodedData,
returnData,
}
} catch (err) {
return {
success: false,
data: null,
returnData,
error: err instanceof Error ? err : new WhiteChainError(String(err)),
}
}
}

return {
success: true,
data: returnData,
returnData,
}
})

return results as any
}
}

/**
* Factory helper to construct a Multicall instance.
*/
export function createMulticall(options?: { publicClient?: PublicClient; multicallAddress?: Address }): Multicall {
return new Multicall(options)
}
14 changes: 13 additions & 1 deletion src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
export { Contract } from './Contract.js'
export { Contract, type ContractClient } from './Contract.js'
export { WhitechainSDK, type WhitechainSDKConfig, type WhitechainSDKPlugins } from './WhitechainSDK.js'

export {
Multicall,
createMulticall,
MULTICALL3_DEFAULT_ADDRESS,
MULTICALL3_ABI,
} from './Multicall.js'

export type {
MulticallCall,
MulticallResult,
MulticallOptions,
} from '../types/multicall.js'
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ export {
} from './providers/BrowserProvider.js'

export {
NonceManager,
createNonceManager,
type NonceManagerOptions,
type GetOnChainNonceFn,
} from './wallet/index.js'


IpcProvider,
type IpcProviderOptions,
} from './providers/IpcProvider.js'
Expand Down
47 changes: 47 additions & 0 deletions src/types/multicall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Address, Hex, PublicClient } from 'viem'
import type { WhiteChainError } from '../types.js'

/**
* A single call payload to be batched via Multicall3.
*/
export type MulticallCall<T = any> = {
/** The target contract address for the view call. */
target: Address
/** The encoded ABI function call payload. */
callData: Hex
/**
* If true (default: true), a revert in this specific call will not cause the
* overall batch to revert.
*/
allowFailure?: boolean
/**
* Optional decoder function to convert returned `Hex` bytes into a typed object.
*/
decoder?: (returnData: Hex) => T
}

/**
* The result of a single call batched via Multicall3.
*/
export type MulticallResult<T = any> = {
/** Whether the specific call succeeded on-chain. */
success: boolean
/** The decoded return data if successful and decoder returned, otherwise raw Hex or null. */
data: T | null
/** The raw returned bytes from the call execution. */
returnData: Hex
/** Error object if the call reverted or decoding failed. */
error?: WhiteChainError | Error
}

/**
* Options for configuring Multicall3 execution.
*/
export type MulticallOptions = {
/** Override the Multicall3 contract address. */
multicallAddress?: Address
/** PublicClient instance to execute the eth_call request. */
publicClient?: PublicClient
/** Default allowFailure setting for calls in this batch if call.allowFailure is omitted. */
allowFailure?: boolean
}
Loading