|
| 1 | +# v1.4.4 Release Notes |
| 2 | + |
| 3 | +## New Features |
| 4 | + |
| 5 | +### Account Abstraction (AA) Wallet Support — `txHashResolver` |
| 6 | + |
| 7 | +Added support for Account Abstraction wallets (e.g. **ZeroDev**, **Dynamic Global Wallet**) via a new optional `txHashResolver` configuration parameter. |
| 8 | + |
| 9 | +#### Problem |
| 10 | + |
| 11 | +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. |
| 12 | + |
| 13 | +#### Solution |
| 14 | + |
| 15 | +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**. |
| 16 | + |
| 17 | +#### Usage |
| 18 | + |
| 19 | +**Normal wallets** — no changes needed: |
| 20 | + |
| 21 | +```typescript |
| 22 | +const client = StoryClient.newClient({ |
| 23 | + transport: http("https://aeneid.storyrpc.io"), |
| 24 | + account: privateKeyToAccount("0x..."), |
| 25 | +}); |
| 26 | +``` |
| 27 | + |
| 28 | +**ZeroDev AA wallet — Using Kernel Client directly (recommended)**: |
| 29 | + |
| 30 | +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: |
| 31 | + |
| 32 | +```typescript |
| 33 | +import { createKernelAccountClient } from "@zerodev/sdk"; |
| 34 | + |
| 35 | +const kernelClient = await createKernelAccountClient({ /* ... */ }); |
| 36 | + |
| 37 | +const client = StoryClient.newClientUseWallet({ |
| 38 | + transport: http("https://aeneid.storyrpc.io"), |
| 39 | + wallet: kernelClient, |
| 40 | + // No txHashResolver needed — kernel client handles it internally |
| 41 | +}); |
| 42 | + |
| 43 | +// All SDK methods work as usual |
| 44 | +const result = await client.ipAsset.mintAndRegisterIpAssetWithPilTerms({ |
| 45 | + spgNftContract: "0x...", |
| 46 | + terms: [], |
| 47 | +}); |
| 48 | +``` |
| 49 | + |
| 50 | +**Raw UserOp wallet + txHashResolver (for AA wallets that return userOpHash)**: |
| 51 | + |
| 52 | +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: |
| 53 | + |
| 54 | +```typescript |
| 55 | +const client = StoryClient.newClientUseWallet({ |
| 56 | + transport: http("https://aeneid.storyrpc.io"), |
| 57 | + wallet: rawAAWallet, // writeContract returns userOpHash |
| 58 | + txHashResolver: async (userOpHash) => { |
| 59 | + const receipt = await bundlerClient.waitForUserOperationReceipt({ |
| 60 | + hash: userOpHash, |
| 61 | + }); |
| 62 | + return receipt.receipt.transactionHash; |
| 63 | + }, |
| 64 | +}); |
| 65 | +``` |
| 66 | + |
| 67 | +> **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. |
| 68 | +
|
| 69 | +**Dynamic Global Wallet**: |
| 70 | + |
| 71 | +```typescript |
| 72 | +const client = StoryClient.newClientUseWallet({ |
| 73 | + transport: http("https://aeneid.storyrpc.io"), |
| 74 | + wallet: dynamicWalletClient, |
| 75 | + txHashResolver: async (userOpHash) => { |
| 76 | + // Use Dynamic's bundler client to resolve the hash |
| 77 | + const receipt = await dynamicBundlerClient.waitForUserOperationReceipt({ |
| 78 | + hash: userOpHash, |
| 79 | + }); |
| 80 | + return receipt.receipt.transactionHash; |
| 81 | + }, |
| 82 | +}); |
| 83 | +``` |
| 84 | + |
| 85 | +#### Supported Factory Methods |
| 86 | + |
| 87 | +`txHashResolver` is supported by all three client creation methods: |
| 88 | + |
| 89 | +- `StoryClient.newClient({ ..., txHashResolver })` |
| 90 | +- `StoryClient.newClientUseWallet({ ..., txHashResolver })` |
| 91 | +- `StoryClient.newClientUseAccount({ ..., txHashResolver })` |
| 92 | + |
| 93 | +#### API Reference |
| 94 | + |
| 95 | +```typescript |
| 96 | +/** |
| 97 | + * A function that resolves a hash returned by writeContract into an actual |
| 98 | + * transaction hash that can be used with waitForTransactionReceipt. |
| 99 | + * |
| 100 | + * @param hash - The hash returned by writeContract (could be a userOpHash or txHash) |
| 101 | + * @returns The resolved on-chain transaction hash |
| 102 | + */ |
| 103 | +type TxHashResolver = (hash: Hash) => Promise<Hash>; |
| 104 | +``` |
| 105 | + |
| 106 | +## Changed Files |
| 107 | + |
| 108 | +| File | Change | |
| 109 | +|------|--------| |
| 110 | +| `packages/core-sdk/src/types/config.ts` | Added `TxHashResolver` type; added `txHashResolver` to `StoryConfig`, `UseWalletStoryConfig`, `UseAccountStoryConfig` | |
| 111 | +| `packages/core-sdk/src/client.ts` | Added `applyTxHashResolver()` method; patched `rpcClient.waitForTransactionReceipt` when resolver is provided; forwarded resolver in factory methods | |
| 112 | +| `packages/core-sdk/test/unit/client.test.ts` | Added unit tests that verify resolver wiring, hash transformation, and constructor-time patching | |
| 113 | +| `packages/core-sdk/test/integration/txHashResolver.test.ts` | ZeroDev E2E integration tests (6 passing), covering both AA wallet modes | |
| 114 | +| `packages/core-sdk/package.json` | Added `@zerodev/sdk`, `@zerodev/ecdsa-validator` as devDependencies | |
| 115 | +| `packages/core-sdk/src/utils/oov3.ts` | Added retry logic (up to 5 attempts) to `settleAssertion()` for `Assertion not expired` errors | |
| 116 | +| `packages/core-sdk/test/unit/utils/oov3.test.ts` | Added `chai-as-promised`; added unit tests for retry success and max-retries failure | |
| 117 | +| `packages/core-sdk/test/integration/dispute.test.ts` | Increased assertion expiry wait from 3s to 15s | |
| 118 | +| `packages/core-sdk/test/integration/group.test.ts` | Added `this.retries(2)` to flaky royalty/reward tests | |
| 119 | +| `packages/core-sdk/test/integration/ipAsset.test.ts` | Added `this.retries(2)` to batch register test | |
| 120 | +| `.github/workflows/pr-internal.yml` | Changed to `secrets: inherit` to forward all repository secrets | |
| 121 | + |
| 122 | +## CI / Test Stability Improvements |
| 123 | + |
| 124 | +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. |
| 125 | + |
| 126 | +### Fix #1 — Dispute: `Assertion not expired` |
| 127 | + |
| 128 | +**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. |
| 129 | + |
| 130 | +**Fix**: |
| 131 | +- `src/utils/oov3.ts` — `settleAssertion()` now retries up to 5 times (with 3s delay) when encountering `Assertion not expired`, instead of failing immediately. |
| 132 | +- `test/integration/dispute.test.ts` — Increased the post-dispute wait from **3s → 15s** in both `beforeEach` and the multicall tagging test. |
| 133 | +- `test/unit/utils/oov3.test.ts` — Added unit tests for retry success and max-retries failure; fixed `chai-as-promised` import. |
| 134 | + |
| 135 | +### Fix #2–#4 — Group: `receiver IP not registered` + cascading failures |
| 136 | + |
| 137 | +**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. |
| 138 | + |
| 139 | +**Fix**: |
| 140 | +- `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. |
| 141 | + |
| 142 | +### Fix #5 — IP Asset: `replacement transaction underpriced` |
| 143 | + |
| 144 | +**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. |
| 145 | + |
| 146 | +**Fix**: |
| 147 | +- `test/integration/ipAsset.test.ts` — Added `this.retries(2)` to the `should successfully register IP assets with multicall disabled` test. |
| 148 | + |
| 149 | +### CI Workflow: `secrets: inherit` |
| 150 | + |
| 151 | +- `.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. |
| 152 | + |
| 153 | +## Test Coverage Notes |
| 154 | + |
| 155 | +- **Unit tests**: Validate that `txHashResolver` is invoked before receipt polling and that the resolved hash is used for receipt lookup. |
| 156 | +- **Integration tests — Pass-through & Simulated**: Cover normal wallet pass-through and simulated AA hash mapping behavior. |
| 157 | +- **Integration tests — ZeroDev E2E (2 scenarios)**: |
| 158 | + - **Kernel client as wallet**: Verifies that `writeContract` returns a real txHash and the SDK works correctly without a resolver. |
| 159 | + - **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. |
| 160 | +- ZeroDev E2E tests require `BUNDLER_RPC_URL` and `WALLET_PRIVATE_KEY` in `.env`. Tests are automatically skipped when these are not configured. |
0 commit comments