Skip to content
Merged
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
27 changes: 24 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from './types.js'
import { WhiteChainError, ValidationError } from './errors/index.js'
import { parseContractError } from './utils/errorHandler.js'
import { formatBigIntToString } from './utils/formatters.js'
import { networks, type NetworkProfile } from './config/networks.js'
import { Eip1193Provider } from './providers/BrowserProvider.js'
import { withGasEstimation, type WithGasEstimation } from './core/TransactionHelper.js'
Expand Down Expand Up @@ -433,7 +434,11 @@ export function createWhiteChainClient(config: WhiteChainConfig & { provider?: a
args: [grantId],
})) as readonly [number, bigint]
const statusMap: Record<number, GrantRound['status']> = { 0: 'open', 1: 'closed', 2: 'archived' }
return { id: grantId, status: statusMap[status] ?? 'open', applicationsCount }
return formatBigIntToString({
id: grantId,
status: statusMap[status] ?? 'open',
applicationsCount,
} satisfies { id: bigint; status: GrantRound['status']; applicationsCount: bigint })
},

async getGrantApplication(applicationId) {
Expand Down Expand Up @@ -466,7 +471,17 @@ export function createWhiteChainClient(config: WhiteChainConfig & { provider?: a
1: 'approved',
2: 'rejected',
}
return { id: applicationId, applicant, status: statusMap[status] ?? 'submitted', metadataUri }
return formatBigIntToString({
id: applicationId,
applicant,
status: statusMap[status] ?? 'submitted',
metadataUri,
} satisfies {
id: bigint
applicant: Address
status: GrantApplication['status']
metadataUri: string
})
},

async getMilestones(applicationId) {
Expand Down Expand Up @@ -503,11 +518,17 @@ export function createWhiteChainClient(config: WhiteChainConfig & { provider?: a
3: 'paid',
}

return raw.map(([id, status, evidenceUri]) => ({
const milestones: Array<{
id: bigint
status: Milestone['status']
evidenceUri: string | undefined
}> = raw.map(([id, status, evidenceUri]) => ({
id,
status: statusMap[status] ?? 'pending',
evidenceUri: evidenceUri || undefined,
}))

return formatBigIntToString(milestones)
},
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export {
type WhiteChainClient,
} from './client.js'
export { formatUnits, parseUnits } from './utils/math.js'
export { formatBigIntToString, type BigIntToString } from './utils/formatters.js'
export {
signERC20Permit,
splitSignature,
Expand Down
8 changes: 4 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,17 @@ export type ReleasePayoutParams = {
/** On-chain state of a grant round, as returned by {@link WhiteChainClient}.`getGrantRound`. */
export type GrantRound = {
/** The grant round's identifier (echoed back from the request). */
id: GrantId
id: string
/** Current lifecycle state of the round. */
status: 'open' | 'closed' | 'archived'
/** Number of applications submitted to this round. */
applicationsCount: bigint
applicationsCount: string
}

/** On-chain state of a grant application, as returned by {@link WhiteChainClient}.`getGrantApplication`. */
export type GrantApplication = {
/** The application's identifier (echoed back from the request). */
id: ApplicationId
id: string
/** Address that submitted the application. */
applicant: Address
/** Current review state of the application. */
Expand All @@ -139,7 +139,7 @@ export type GrantApplication = {
/** On-chain state of a milestone, as returned by {@link WhiteChainClient}.`getMilestones`. */
export type Milestone = {
/** The milestone's identifier. */
id: MilestoneId
id: string
/** Current lifecycle state of the milestone. */
status: 'pending' | 'evidence-submitted' | 'approved' | 'paid'
/** URI of the submitted evidence, if any has been submitted yet. */
Expand Down
25 changes: 25 additions & 0 deletions src/utils/formatters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export type BigIntToString<T> = T extends bigint
? string
: T extends readonly unknown[]
? { [Key in keyof T]: BigIntToString<T[Key]> }
: T extends object
? { [Key in keyof T]: BigIntToString<T[Key]> }
: T

export function formatBigIntToString<T>(value: T): BigIntToString<T> {
if (typeof value === 'bigint') {
return value.toString() as BigIntToString<T>
}

if (Array.isArray(value)) {
return value.map((item) => formatBigIntToString(item)) as BigIntToString<T>
}

if (value !== null && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, formatBigIntToString(item)])
) as BigIntToString<T>
}

return value as BigIntToString<T>
}
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { formatUnits, parseUnits } from './math.js'
export { formatBigIntToString, type BigIntToString } from './formatters.js'
export {
signERC20Permit,
splitSignature,
Expand Down
12 changes: 7 additions & 5 deletions tests/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,19 @@ describe('WhiteChainClient', () => {
})

const round = await client.getGrantRound(10n)
expect(round).toEqual({ id: 10n, status: 'closed', applicationsCount: 5n })
expect(round).toEqual({ id: '10', status: 'closed', applicationsCount: '5' })
expect(() => JSON.stringify(round)).not.toThrow()

const app = await client.getGrantApplication(3n)
expect(app).toEqual({ id: 3n, applicant: grantAddress, status: 'submitted', metadataUri: 'ipfs://meta' })
expect(app).toEqual({ id: '3', applicant: grantAddress, status: 'submitted', metadataUri: 'ipfs://meta' })
expect(() => JSON.stringify(app)).not.toThrow()

const milestones = await client.getMilestones(3n)
expect(milestones).toEqual([
{ id: 1n, status: 'approved', evidenceUri: 'ipfs://e1' },
{ id: 2n, status: 'paid', evidenceUri: undefined },
{ id: '1', status: 'approved', evidenceUri: 'ipfs://e1' },
{ id: '2', status: 'paid', evidenceUri: undefined },
])
expect(() => JSON.stringify(milestones)).not.toThrow()
})

it('updates clients dynamically when switchNetwork is called', async () => {
Expand All @@ -117,4 +120,3 @@ describe('WhiteChainClient', () => {
expect(client.publicClient).not.toBe(originalPublicClient)
})
})

35 changes: 35 additions & 0 deletions tests/utils/formatters.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest'
import { formatBigIntToString } from '../../src/utils/formatters.js'

describe('formatBigIntToString', () => {
it('converts deeply nested bigint values without losing precision', () => {
const value = {
amount: 90071992547409931234567890n,
nested: {
values: [1n, { balance: -2n }],
label: 'unchanged',
},
optional: undefined,
empty: null,
}

const formatted = formatBigIntToString(value)

expect(formatted).toEqual({
amount: '90071992547409931234567890',
nested: {
values: ['1', { balance: '-2' }],
label: 'unchanged',
},
optional: undefined,
empty: null,
})
expect(() => JSON.stringify(formatted)).not.toThrow()
})

it('returns primitive values unchanged', () => {
expect(formatBigIntToString('whitechain')).toBe('whitechain')
expect(formatBigIntToString(42)).toBe(42)
expect(formatBigIntToString(false)).toBe(false)
})
})