Skip to content

feat(sdk): license-token minting with automatic WIP fee handling#140

Merged
yingyangxu2026 merged 3 commits into
mainfrom
feat/39-license-mint-helper
Jul 23, 2026
Merged

feat(sdk): license-token minting with automatic WIP fee handling#140
yingyangxu2026 merged 3 commits into
mainfrom
feat/39-license-mint-helper

Conversation

@sophiej-story

@sophiej-story sophiej-story commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #39. Reading a LicenseReadCondition-gated vault requires holding a Story Protocol license token, and minting one charges a fee the RoyaltyModule pulls in WIP (the ERC-20 wrapper of the native token). A user with only native balance and no WIP hit a bare InsufficientBalance revert and had to run three manual transactions — WIP.deposit(), WIP.approve(RoyaltyModule, fee), LicensingModule.mintLicenseTokens(...) — or pull in the heavy @story-protocol/core-sdk.

This adds mintLicenseToken, which does the whole flow in one call: predict the fee on-chain → wrap the missing native DATA into WIP → approve the RoyaltyModule → mint → return the token IDs (ready to encode as accessAuxData for accessCDR). Fee preparation is idempotent — wrap/approve are skipped when balance/allowance already suffice.

Token naming: the native token was renamed $IP → $DATA in the rebrand, but its wrapper contract (0x1514…, immutable) keeps the on-chain name WIP. So the flow wraps native DATA and receives WIP — the name mismatch is expected. Existing license terms are WIP-denominated, so the helper handles WIP only; WDATA (a separate new wrapper) is out of scope, and non-WIP terms surface UnsupportedLicenseCurrencyError.

API

Exposed two ways (mirrors observer/uploader/consumer):

  • client.license.mintLicenseToken({ licensorIpId, licenseTermsId, amount?, receiver?, autoWrap?, autoApprove? })
  • standalone mintLicenseToken({ publicClient, walletClient, ...same }) for callers without a CDRClient

Returns { licenseTokenIds, feePaid, wrappedWei, txHashes }txHashes.deposit/.approve are absent when those steps were skipped.

Design notes

  • Fee is read on-chain (predictMintingLicenseFee), not passed in — the caller shouldn't need to know it. The same call returns the fee currency, which gives the WIP-only check for free (non-WIP terms → UnsupportedLicenseCurrencyError).
  • maxUint256 approval by default so repeat mints skip the approve tx (opt out with autoApprove: false). autoWrap: false likewise turns a shortfall into a typed error instead of wrapping.
  • Predicted fee is passed as maxMintingFee so terms repriced between prediction and execution revert rather than overpay.
  • Story Protocol contracts kept in a self-contained license-contracts.ts (addresses + minimal ABI subsets), NOT in @piplabs/cdr-contracts, so that package stays purely CDR. Addresses are identical on Aeneid and mainnet.
  • Not wired into accessCDR — minting spends money and the read path can't reliably infer mint parameters, so it stays an explicit user-invoked helper. (A future non-spending enhancement could make accessCDR point at it when a license-gated read fails.)
  • Multicall batching and @story-protocol/core-sdk deliberately avoided — sequential txs keep the new external surface to the addresses above.

Tests

  • 16 unit tests (license.test.ts): free terms, sufficient-WIP fast path, approve-then-mint, exact-shortfall wrap, insufficient-native (typed error, zero txs), non-WIP currency, both opt-out flags, amount > 1 with full mint-arg assertion, missing mint event, reverted deposit/mint (step-labeled error, no cascade), standalone function, wallet-required getter.
  • Integration DX-03 un-skipped & parametrized (dx-improvements.test.ts): runs the full LicenseReadCondition journey (upload gated vault → unlicensed read rejects → fresh reader mints via the helper → gated read recovers the dataKey) over two Aeneid fixtures — free terms (2054) and a fee-bearing pair (0x2dfC…/2795, 1 WIP, currency + fee verified on-chain via predictMintingLicenseFee). Free asserts the wrap/approve skip path; fee-bearing asserts wrap→approve→pay (feePaid > 0, wrappedWei === feePaid, deposit+approve ran).

Test plan

Review fixes (jinn-agent)

  • Dropped the license-contracts re-export from index.ts — raw ABIs/addresses stay out of the public semver surface.
  • Reject amount < 1 or > Number.MAX_SAFE_INTEGER (guards the Number(amount) token-id expansion).
  • Log license.wrap.preflight.skipped when getBalance is absent, for parity with Consumer.preflightBalance.
  • DX-03's fee assertion was mis-scoped (2054 is free on-chain, so feePaid > 0 falsely failed). Resolved by parametrizing over a free fixture (asserts wrap/approve skipped) and a verified fee-bearing fixture (asserts the fee path), rather than assuming any single terms carries a fee.

@jinn-agent

jinn-agent Bot commented Jul 20, 2026

Copy link
Copy Markdown

The implementation is well-structured and the test coverage is comprehensive. Two correctness issues are worth addressing before merge:

  1. Non-integer amount input throws a native RangeError instead of InvalidParamsErrorBigInt(1.5) throws RangeError: The number 1.5 cannot be converted to a BigInt because it is not an integer, bypassing the typed error path entirely.

  2. Integration test silently passes on non-Aeneid chainsrunLicenseGatedFlow returns Promise<void> (resolves) when chainId !== 1315, so the test reports green with zero assertions, masking misconfigured environments.

Everything else (fee flow logic, idempotency, revert detection, event decoding, address constants, public surface) looks correct and well-tested.


Review iteration 3 · Commit e9c4e1c · 2026-07-21T07:12:58Z

@yingyangxu2026
yingyangxu2026 merged commit 876ad09 into main Jul 23, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: auto-wrap DATA to WDATA for license token minting fee

2 participants