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
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @LeoHChen @DonFungible @jacob-tucker @allenchuang @limengformal @lucas2brh @yingyangxu2026 @bonnie57
* @LeoHChen @DonFungible @jacob-tucker @allenchuang @limengformal @lucas2brh @yingyangxu2026 @chao-peng-story @roycezhaoca
19 changes: 16 additions & 3 deletions .github/workflows/pr-internal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,24 @@ on:
pull_request:
branches:
- main
paths-ignore:
- '.github/**'
- '.github/CODEOWNERS'
- '**/*.md'
- 'docs/**'
- '.gitignore'
- 'LICENSE'

push:
branches:
- main
paths-ignore:
- '.github/**'
- '.github/CODEOWNERS'
- '**/*.md'
- 'docs/**'
- '.gitignore'
- 'LICENSE'

jobs:
Timestamp:
Expand All @@ -19,6 +34,4 @@ jobs:
with:
sha: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
ENVIRONMENT: "odyssey"
secrets:
WALLET_PRIVATE_KEY: ${{ secrets.WALLET_PRIVATE_KEY }}
TEST_WALLET_ADDRESS: ${{ secrets.TEST_WALLET_ADDRESS }}
secrets: inherit
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
node_modules
.pnp
.pnp.js
.pnpm-store

# testing
coverage
Expand Down
1 change: 0 additions & 1 deletion .husky/pre-commit

This file was deleted.

7 changes: 0 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,3 @@ repos:
hooks:
- id: gitleaks
args: ['--verbose', '--redact']
- repo: local
hooks:
- id: lint-staged
name: lint-staged (ESLint)
entry: pnpm run lint-staged
language: system
pass_filenames: false
1 change: 0 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ This section provides the instructions on how to build Story Protocol SDK from s
- Install PNPM: Execute `npm install -g pnpm`
- Install TypeScript: Run `pnpm add typescript -D`
- Install Yalc: Use `npm install -g yalc`
- Install dependencies: `pnpm install` (this automatically sets up Husky git hooks for ESLint checks on commit)

#### Steps for Using Yalc for Local Testing of Core-SDK

Expand Down
160 changes: 160 additions & 0 deletions docs/v1.4.4-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# v1.4.4 Release Notes

## New Features

### Account Abstraction (AA) Wallet Support — `txHashResolver`

Added support for Account Abstraction wallets (e.g. **ZeroDev**, **Dynamic Global Wallet**) via a new optional `txHashResolver` configuration parameter.

#### Problem

When using AA wallets, `writeContract` returns a **UserOperation hash** instead of a standard **transaction hash**. The SDK's `waitForTransactionReceipt` only works with regular transaction hashes, causing AA wallet transactions to fail silently or hang.

#### Solution

A new `txHashResolver` option can be passed when creating a `StoryClient`. This function resolves UserOperation hashes into on-chain transaction hashes before the SDK waits for receipts. The resolver is applied transparently — **all existing SDK methods work without any changes**.

#### Usage

**Normal wallets** — no changes needed:

```typescript
const client = StoryClient.newClient({
transport: http("https://aeneid.storyrpc.io"),
account: privateKeyToAccount("0x..."),
});
```

**ZeroDev AA wallet — Using Kernel Client directly (recommended)**:

ZeroDev's `createKernelAccountClient` returns a viem smart account client whose `writeContract` internally sends the UserOperation, waits for the bundler to include it on-chain, and returns the **real tx hash**. Therefore **`txHashResolver` is NOT needed** — simply pass the kernel client as the wallet:

```typescript
import { createKernelAccountClient } from "@zerodev/sdk";

const kernelClient = await createKernelAccountClient({ /* ... */ });

const client = StoryClient.newClientUseWallet({
transport: http("https://aeneid.storyrpc.io"),
wallet: kernelClient,
// No txHashResolver needed — kernel client handles it internally
});

// All SDK methods work as usual
const result = await client.ipAsset.mintAndRegisterIpAssetWithPilTerms({
spgNftContract: "0x...",
terms: [],
});
```

**Raw UserOp wallet + txHashResolver (for AA wallets that return userOpHash)**:

Some AA wallet integrations return the **UserOperation hash** directly from `writeContract` without internally waiting for the bundler receipt. For these wallets, configure `txHashResolver` to convert the userOpHash into the real on-chain tx hash:

```typescript
const client = StoryClient.newClientUseWallet({
transport: http("https://aeneid.storyrpc.io"),
wallet: rawAAWallet, // writeContract returns userOpHash
txHashResolver: async (userOpHash) => {
const receipt = await bundlerClient.waitForUserOperationReceipt({
hash: userOpHash,
});
return receipt.receipt.transactionHash;
},
});
```

> **How to tell?** If the hash returned by the AA wallet's `writeContract` cannot be found as a transaction on a block explorer, it is a userOpHash and you need `txHashResolver`. If it can be found on-chain directly, the wallet already handles resolution internally and no resolver is needed.

**Dynamic Global Wallet**:

```typescript
const client = StoryClient.newClientUseWallet({
transport: http("https://aeneid.storyrpc.io"),
wallet: dynamicWalletClient,
txHashResolver: async (userOpHash) => {
// Use Dynamic's bundler client to resolve the hash
const receipt = await dynamicBundlerClient.waitForUserOperationReceipt({
hash: userOpHash,
});
return receipt.receipt.transactionHash;
},
});
```

#### Supported Factory Methods

`txHashResolver` is supported by all three client creation methods:

- `StoryClient.newClient({ ..., txHashResolver })`
- `StoryClient.newClientUseWallet({ ..., txHashResolver })`
- `StoryClient.newClientUseAccount({ ..., txHashResolver })`

#### API Reference

```typescript
/**
* A function that resolves a hash returned by writeContract into an actual
* transaction hash that can be used with waitForTransactionReceipt.
*
* @param hash - The hash returned by writeContract (could be a userOpHash or txHash)
* @returns The resolved on-chain transaction hash
*/
type TxHashResolver = (hash: Hash) => Promise<Hash>;
```

## Changed Files

| File | Change |
|------|--------|
| `packages/core-sdk/src/types/config.ts` | Added `TxHashResolver` type; added `txHashResolver` to `StoryConfig`, `UseWalletStoryConfig`, `UseAccountStoryConfig` |
| `packages/core-sdk/src/client.ts` | Added `applyTxHashResolver()` method; patched `rpcClient.waitForTransactionReceipt` when resolver is provided; forwarded resolver in factory methods |
| `packages/core-sdk/test/unit/client.test.ts` | Added unit tests that verify resolver wiring, hash transformation, and constructor-time patching |
| `packages/core-sdk/test/integration/txHashResolver.test.ts` | ZeroDev E2E integration tests (6 passing), covering both AA wallet modes |
| `packages/core-sdk/package.json` | Added `@zerodev/sdk`, `@zerodev/ecdsa-validator` as devDependencies |
| `packages/core-sdk/src/utils/oov3.ts` | Added retry logic (up to 5 attempts) to `settleAssertion()` for `Assertion not expired` errors |
| `packages/core-sdk/test/unit/utils/oov3.test.ts` | Added `chai-as-promised`; added unit tests for retry success and max-retries failure |
| `packages/core-sdk/test/integration/dispute.test.ts` | Increased assertion expiry wait from 3s to 15s |
| `packages/core-sdk/test/integration/group.test.ts` | Added `this.retries(2)` to flaky royalty/reward tests |
| `packages/core-sdk/test/integration/ipAsset.test.ts` | Added `this.retries(2)` to batch register test |
| `.github/workflows/pr-internal.yml` | Changed to `secrets: inherit` to forward all repository secrets |

## CI / Test Stability Improvements

This release also includes fixes for 5 flaky integration tests that were failing due to testnet timing and nonce contention issues. These failures were **pre-existing** and unrelated to the `txHashResolver` feature.

### Fix #1 — Dispute: `Assertion not expired`

**Root cause**: `settleAssertion` was called before the on-chain assertion liveness period had actually expired. The previous 3-second wait was insufficient given variable testnet block times.

**Fix**:
- `src/utils/oov3.ts` — `settleAssertion()` now retries up to 5 times (with 3s delay) when encountering `Assertion not expired`, instead of failing immediately.
- `test/integration/dispute.test.ts` — Increased the post-dispute wait from **3s → 15s** in both `beforeEach` and the multicall tagging test.
- `test/unit/utils/oov3.test.ts` — Added unit tests for retry success and max-retries failure; fixed `chai-as-promised` import.

### Fix #2–#4 — Group: `receiver IP not registered` + cascading failures

**Root cause**: Nonce contention from a shared test wallet caused intermittent transaction failures. When `payRoyaltyOnBehalf` failed in the "collect royalties" test, the subsequent "get claimable reward" and "claim reward" tests also failed because they depended on the state from the first test.

**Fix**:
- `test/integration/group.test.ts` — Added `this.retries(2)` to all three tests (`collect royalties`, `get claimable reward`, `claim reward`). Mocha will automatically retry each test up to 2 additional times on failure.

### Fix #5 — IP Asset: `replacement transaction underpriced`

**Root cause**: Nonce collision — a pending transaction from a previous test occupied the same nonce with equal or higher gas, causing the RPC node to reject the replacement.

**Fix**:
- `test/integration/ipAsset.test.ts` — Added `this.retries(2)` to the `should successfully register IP assets with multicall disabled` test.

### CI Workflow: `secrets: inherit`

- `.github/workflows/pr-internal.yml` — Changed from explicitly listing individual secrets to `secrets: inherit`, so all repository secrets (including `BUNDLER_RPC_URL`) are automatically forwarded to the reusable workflow.

## Test Coverage Notes

- **Unit tests**: Validate that `txHashResolver` is invoked before receipt polling and that the resolved hash is used for receipt lookup.
- **Integration tests — Pass-through & Simulated**: Cover normal wallet pass-through and simulated AA hash mapping behavior.
- **Integration tests — ZeroDev E2E (2 scenarios)**:
- **Kernel client as wallet**: Verifies that `writeContract` returns a real txHash and the SDK works correctly without a resolver.
- **Raw userOp wallet + txHashResolver**: Uses a custom wallet wrapper (whose `writeContract` returns a userOpHash) to verify end-to-end that the resolver converts the userOpHash into a real on-chain txHash.
- ZeroDev E2E tests require `BUNDLER_RPC_URL` and `WALLET_PRIVATE_KEY` in `.env`. Tests are automatically skipped when these are not configured.
10 changes: 1 addition & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,14 @@
"scripts": {
"build": "turbo run build",
"lint": "turbo run lint",
"lint-staged": "node scripts/precommit-run-eslint.mjs",
"fix": "turbo run fix",
"test": "turbo run test --no-cache --concurrency=1",
"test:integration": "turbo run test:integration --no-cache",
"prepare": "husky"
"test:integration": "turbo run test:integration --no-cache"
},
"devDependencies": {
"@changesets/cli": "2.29.6",
"eslint": "^9.26.0",
"husky": "^9.1.7",
"lint-staged": "^15.2.1",
"turbo": "^1.10.13"
},
"lint-staged": {
"packages/core-sdk/**/*.{ts,tsx,js,jsx,mjs,cjs}": "node scripts/precommit-eslint-core-sdk.mjs"
},
"engines": {
"node": "^20.0.0"
},
Expand Down
2 changes: 2 additions & 0 deletions packages/core-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
"@types/mocha": "^10.0.2",
"@types/node": "^20.8.2",
"@types/sinon": "^10.0.18",
"@zerodev/ecdsa-validator": "^5.4.9",
"@zerodev/sdk": "^5.5.7",
"c8": "^8.0.1",
"chai": "^4.3.10",
"chai-as-promised": "^7.1.1",
Expand Down
Loading
Loading