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
5 changes: 5 additions & 0 deletions .changeset/tidy-dodos-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'ox': patch
---

Restored number-valued SIWE chain IDs for message creation and parsing.
5 changes: 5 additions & 0 deletions site/src/pages/_root.css
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ body,
--vocs-spacing-content: var(--max-w);
}

/* Account for the fixed banner on the landing page. */
[data-layout='blank']:has(.landing-pattern) > [data-v-main] {
padding-top: var(--vocs-spacing-banner);
}

/* Landing page: tighten top padding above the header */
[data-layout='blank']:has(.landing-pattern) [data-v-content] {
padding-top: 2em;
Expand Down
6 changes: 3 additions & 3 deletions site/src/pages/guides/siwe.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Before you can create a SIWE message, you need to source the following informati
function onClick() {
const data = {
address: '<connected address>', // e.g. Wagmi `useAccount()`/`getAccount()`
chainId: 1n,
chainId: 1,
domain: window.location.host,
nonce: '<fetched from server>', // e.g. `await getNonceFromServer()`
uri: window.location.origin,
Expand All @@ -62,7 +62,7 @@ import { Siwe } from 'ox'
function onClick() {
const data = {
address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
chainId: 1n,
chainId: 1,
domain: 'example.com',
nonce: '65ed4681d4efe0270b923ff5f4b097b1c95974dc33aeebecd5724c42fd86dfd25dc70b27ef836b2aa22e68f19ebcccc1',
uri: 'https://example.com/path',
Expand Down Expand Up @@ -94,7 +94,7 @@ import { Hex, Provider, Siwe } from 'ox'
async function onClick() {
const data = {
address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
chainId: 1n,
chainId: 1,
domain: 'example.com',
nonce: '65ed4681d4efe0270b923ff5f4b097b1c95974dc33aeebecd5724c42fd86dfd25dc70b27ef836b2aa22e68f19ebcccc1',
uri: 'https://example.com/path',
Expand Down
184 changes: 184 additions & 0 deletions site/src/pages/migrating-from-v0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
---
description: "Migrate applications from Ox v0 to v1."
---

# Migrating from v0

Ox v1 includes breaking changes to decoded addresses, cryptographic values, blob handling, and Tempo types. This guide covers every breaking change and the migration required for each one.

First, upgrade Ox to v1.

:::code-group
```bash [pnpm]
pnpm add ox@^1
```

```bash [npm]
npm install ox@^1
```

```bash [yarn]
yarn add ox@^1
```

```bash [bun]
bun add ox@^1
```
:::

## ABI addresses are checksummed

ABI decode functions now checksum decoded addresses by default. If your application compares decoded addresses as case-sensitive strings, normalize both sides or pass `checksumAddress: false` to preserve the v0 behavior.

```ts
const values = AbiParameters.decode(parameters, data) // [!code --]
const values = AbiParameters.decode(parameters, data, { // [!code ++]
checksumAddress: false, // [!code ++]
}) // [!code ++]
```

The option is also available on the higher-level ABI decode functions that return addresses.

## Cryptographic coordinates use padded hex

ECDSA and BLS coordinate fields now use padded `Hex.Hex` strings instead of `bigint`. This includes `r`, `s`, `x`, `y`, and BLS `Fp`/`Fp2` values on `Signature`, `PublicKey`, `BlsPoint`, `Transaction`, `Authorization`, `TxEnvelope`, and related Tempo and ERC types.

ECDSA coordinates are 32 bytes. BLS12-381 coordinates are 48 bytes. The `bigintType` generic has been removed.

```ts
const signature = Signature.from({ // [!code --]
r: 0x6e100a352ec6ad1b70802290e18aeed190704973570f3b8ed42cb9808e2ea6bfn, // [!code --]
s: 0x4a90a229a244495b41890987806fcbd2d5d23fc0dbe5f5256c2613c039d76db8n, // [!code --]
yParity: 1, // [!code --]
}) // [!code --]
const signature = Signature.from({ // [!code ++]
r: '0x6e100a352ec6ad1b70802290e18aeed190704973570f3b8ed42cb9808e2ea6bf', // [!code ++]
s: '0x4a90a229a244495b41890987806fcbd2d5d23fc0dbe5f5256c2613c039d76db8', // [!code ++]
yParity: 1, // [!code ++]
}) // [!code ++]
```

Use `Hex.fromNumber(value, { size: 32 })` for ECDSA values and `Hex.fromNumber(value, { size: 48 })` for BLS12-381 values when migrating stored bigints.

## Noble and Scure dependencies use v2

Ox now uses v2 of `@noble/ciphers`, `@noble/curves`, `@noble/hashes`, `@scure/bip32`, and `@scure/bip39`.

ECDSA signatures now default to `lowS: true` for both `Secp256k1` and `P256`. In v0, `P256` signatures could have high-S values.

If you use the `noble` re-exports on `Secp256k1`, `P256`, `Ed25519`, `X25519`, or `Bls`, migrate to the noble v2 API. Notable renames include:

| v0 | v1 |
| --- | --- |
| `ProjectivePoint` or `ExtendedPoint` | `Point` |
| `bls.sign` and `bls.verify` | `bls.longSignatures.sign` and `bls.longSignatures.verify` |

Refer to the relevant noble and Scure v2 changelogs for the complete upstream API changes.

## PeerDAS replaces blob sidecars

The EIP-4844 blob-sidecar API has been removed in favor of PeerDAS (EIP-7594).

- `Kzg.Kzg` no longer includes `computeBlobKzgProof`.
- `Blobs.toSidecars`, `Blobs.toProofs`, and `Blobs.sidecarsToVersionedHashes` have been removed.
- `Blobs.BlobSidecar` and `Blobs.BlobSidecars` have been removed.
- `TxEnvelopeEip4844.sidecars` and the legacy network-wrapper serialization have been removed.

Use a PeerDAS-capable KZG backend. It must provide `blobToKzgCommitment`, `computeCells`, `computeCellsAndKzgProofs`, `recoverCellsAndKzgProofs`, and `verifyCellKzgProofBatch`.

Use `BlobCells` to construct and verify cells and data columns.

```ts
const sidecars = Blobs.toSidecars(blobs, { kzg }) // [!code --]
const versionedHashes = Blobs.sidecarsToVersionedHashes(sidecars) // [!code --]
const columns = BlobCells.toDataColumns(blobs, { kzg }) // [!code ++]
const versionedHashes = Blobs.toVersionedHashes(blobs, { kzg }) // [!code ++]
```

`Kzg.Kzg.blobToKzgCommitment` and `Blobs.toVersionedHashes` remain available for transaction versioned-hash derivation.

## Tempo addresses use plain hex

The `TempoAddress` module and its `tempox`-prefixed address format have been removed. Tempo modules now accept plain `Address.Address` hex values.

Remove calls to `TempoAddress.format`, `TempoAddress.parse`, and `TempoAddress.resolve` at application boundaries. The `addressType` generic has also been removed from `Call`, `TxEnvelopeTempo`, `KeyAuthorization`, `AuthorizationTempo`, and `TransactionRequest`.

```ts
import { TempoAddress } from 'ox/tempo' // [!code --]

const formatted = TempoAddress.format(address) // [!code --]
const { address: resolved } = TempoAddress.parse(formatted) // [!code --]
const resolved = address // [!code ++]
```

## Tempo tokens use addresses

The Tempo `TokenId` module has been removed. `feeToken`, `Channel.token`, and the token arguments passed to `PoolId.from` now accept `Address.Address` values instead of numeric token IDs.

```ts
const envelope = TxEnvelopeTempo.from({
// ...
feeToken: 1n, // [!code --]
feeToken: '0x20c0000000000000000000000000000000000001', // [!code ++]
})
```

`Channel.Resolved` has also been removed. Use `Channel.Channel` with address-valued token fields.

## Tempo multisig no longer uses `config_id`

TIP-1061 multisig account addresses now derive directly from the initial configuration. `MultisigConfig.toId` and the `genesisConfigId` fields have been removed.

```ts
const id = MultisigConfig.toId(genesisConfig) // [!code --]
const account = MultisigConfig.getAddress({ genesisConfigId: id }) // [!code --]
const account = MultisigConfig.getAddress(genesisConfig) // [!code ++]

const payload = MultisigConfig.getSignPayload({ // [!code --]
payload: transactionPayload, // [!code --]
account, // [!code --]
genesisConfigId: id, // [!code --]
}) // [!code --]
const payload = MultisigConfig.getSignPayload({ // [!code ++]
payload: transactionPayload, // [!code ++]
account, // [!code ++]
}) // [!code ++]

const envelope = SignatureEnvelope.from({ // [!code --]
account, // [!code --]
genesisConfigId: id, // [!code --]
signatures, // [!code --]
}) // [!code --]
const envelope = SignatureEnvelope.from({ account, signatures }) // [!code ++]
```

Owner approval digests now bind only the account. The signature wire format is `0x05 || rlp([account, signatures, init?])`, `MultisigConfig.maxOwners` is 255 with `u8` weights, and owner approvals may contain nested multisig signatures.

## RPC schema codecs return native quantities

The `ox/zod` RPC schema codecs now decode scalar quantity results to native values. Balance, gas, fee, and block-number quantities decode to `bigint`; chain IDs, transaction counts, and block transaction counts decode to `number`.

Use `RpcSchema.FromZod` when you need the raw JSON-RPC wire types. Use the codec return types when consuming decoded values.

## Transaction envelope type detection expects native types

`TransactionEnvelope.getType` now rejects RPC-style type strings such as `'0x0'` through `'0x4'`. Convert RPC transaction requests before detecting their envelope type.

```ts
const type = TransactionEnvelope.getType(rpcRequest) // [!code --]
const request = TransactionRequest.fromRpc(rpcRequest) // [!code ++]
const type = TransactionEnvelope.getType(request) // [!code ++]
```

## Stricter validation

Ox v1 rejects malformed values that v0 sometimes accepted or normalized. Audit code that relies on permissive parsing, particularly for:

- undersized ABI data, invalid packed-array lengths, and anonymous event topics;
- weak or invalid keystore KDF parameters;
- malformed public keys, blooms, typed data, COSE keys, values, RLP, and JSON-RPC responses;
- non-32-byte access-list storage keys;
- EIP-4844 envelopes without `blobVersionedHashes`; and
- invalid WebAuthn assertion flags or extension data.

See the [v1.0.0 changelog](https://github.com/wevm/ox/blob/main/CHANGELOG.md#100) for the full list of corrected validation and serialization behavior.
6 changes: 6 additions & 0 deletions site/vocs.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export default defineConfig({
}
: {}),
accentColor: 'light-dark(#b8421d, #e85d35)',
banner: {
content: 'Looking for Ox v0? View the v0 documentation.',
dismissId: 'v0-docs',
href: 'https://v0.oxlib.sh',
},
baseUrl: pkg.version.includes('-beta.')
? 'https://v1.oxlib.sh'
: process.env.VERCEL_ENV === 'production'
Expand Down Expand Up @@ -62,6 +67,7 @@ export default defineConfig({
{ text: 'Imports & Bundle Size', link: '/imports' },
{ text: 'Error Handling', link: '/error-handling' },
{ text: 'Platform Compatibility', link: '/platform-compatibility' },
{ text: 'Migrating from v0', link: '/migrating-from-v0' },
{
text: 'Guides',
items: [
Expand Down
12 changes: 6 additions & 6 deletions src/core/Siwe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export type Message = {
/**
* The [EIP-155](https://eips.ethereum.org/EIPS/eip-155) Chain ID to which the session is bound,
*/
chainId: bigint
chainId: number
/**
* [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986) authority that is requesting the signing.
*/
Expand Down Expand Up @@ -88,7 +88,7 @@ export type Message = {
*
* Siwe.createMessage({
* address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
* chainId: 1n,
* chainId: 1,
* domain: 'example.com',
* nonce: 'foobarbaz',
* uri: 'https://example.com/path',
Expand Down Expand Up @@ -126,7 +126,7 @@ export function createMessage(value: Message): string {
// Validate fields
{
// Required fields
if (typeof chainId !== 'bigint' || chainId < 0n)
if (!Number.isInteger(chainId) || chainId < 0)
throw new InvalidMessageFieldError({
field: 'chainId',
metaMessages: [
Expand Down Expand Up @@ -351,7 +351,7 @@ function splitUri(value: string) {
* Issued At: 2023-02-01T00:00:00.000Z`)
* // @log: {
* // @log: address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
* // @log: chainId: 1n,
* // @log: chainId: 1,
* // @log: domain: 'example.com',
* // @log: issuedAt: '2023-02-01T00:00:00.000Z',
* // @log: nonce: 'foobarbaz',
Expand Down Expand Up @@ -387,7 +387,7 @@ export function parseMessage(message: string): ExactPartial<Message> {
return {
...prefix,
...suffix,
...(chainId ? { chainId: BigInt(chainId) } : {}),
...(chainId ? { chainId: Number(chainId) } : {}),
...(expirationTime ? { expirationTime: new Date(expirationTime) } : {}),
...(issuedAt ? { issuedAt: new Date(issuedAt) } : {}),
...(notBefore ? { notBefore: new Date(notBefore) } : {}),
Expand All @@ -410,7 +410,7 @@ export function parseMessage(message: string): ExactPartial<Message> {
* domain: 'example.com',
* message: {
* address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
* chainId: 1n,
* chainId: 1,
* domain: 'example.com',
* nonce: 'foobarbaz',
* uri: 'https://example.com/path',
Expand Down
14 changes: 6 additions & 8 deletions src/core/_test/Siwe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, expect, test, vi } from 'vp/test'
describe('createMessage', () => {
const message = {
address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
chainId: 1n,
chainId: 1,
domain: 'example.com',
nonce: 'foobarbaz',
uri: 'https://example.com/path',
Expand Down Expand Up @@ -277,10 +277,8 @@ describe('createMessage', () => {
})

test('behavior: invalid chainId', () => {
expect(() =>
// @ts-expect-error chain IDs must be bigint.
Siwe.createMessage({ ...message, chainId: 1.1 }),
).toThrowErrorMatchingInlineSnapshot(`
expect(() => Siwe.createMessage({ ...message, chainId: 1.1 }))
.toThrowErrorMatchingInlineSnapshot(`
[Siwe.InvalidMessageFieldError: Invalid Sign-In with Ethereum message field "chainId".

- Chain ID must be a EIP-155 chain ID.
Expand Down Expand Up @@ -478,7 +476,7 @@ Issued At: 2023-02-01T00:00:00.000Z`
expect(parsed).toMatchInlineSnapshot(`
{
"address": "0xA0Cf798816D4b9b9866b5330EEa46a18382f251e",
"chainId": 1n,
"chainId": 1,
"domain": "example.com",
"issuedAt": 2023-02-01T00:00:00.000Z,
"nonce": "foobarbaz",
Expand Down Expand Up @@ -626,7 +624,7 @@ Request ID: 123e4567-e89b-12d3-a456-426614174000`
const parsed = Siwe.parseMessage(message)
expect(parsed).toMatchInlineSnapshot(`
{
"chainId": 1n,
"chainId": 1,
"issuedAt": 2023-02-01T00:00:00.000Z,
"nonce": "foobarbaz",
"requestId": "123e4567-e89b-12d3-a456-426614174000",
Expand All @@ -646,7 +644,7 @@ Request ID: 123e4567-e89b-12d3-a456-426614174000`
describe('validateMessage', () => {
const message = {
address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
chainId: 1n,
chainId: 1,
domain: 'example.com',
nonce: 'foobarbaz',
uri: 'https://example.com/path',
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2472,7 +2472,7 @@ export * as Signature from './core/Signature.js'
*
* Siwe.createMessage({
* address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
* chainId: 1n,
* chainId: 1,
* domain: 'example.com',
* nonce: 'foobarbaz',
* uri: 'https://example.com/path',
Expand Down Expand Up @@ -2544,7 +2544,7 @@ export * as Signature from './core/Signature.js'
* domain: 'example.com',
* message: {
* address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
* chainId: 1n,
* chainId: 1,
* domain: 'example.com',
* nonce: 'foobarbaz',
* uri: 'https://example.com/path',
Expand Down
Loading