You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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):
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
pnpm -r build, pnpm lint, unit tests green (270 sdk + 22 crypto + 21 monitor + 15 cli)
ABI subsets + addresses verified against thedatafoundation/sdk generated bindings
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.
The implementation is well-structured and the test coverage is comprehensive. Two correctness issues are worth addressing before merge:
Non-integer amount input throws a native RangeError instead of InvalidParamsError — BigInt(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.
Integration test silently passes on non-Aeneid chains — runLicenseGatedFlow 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 bareInsufficientBalancerevert 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 asaccessAuxDataforaccessCDR). Fee preparation is idempotent — wrap/approve are skipped when balance/allowance already suffice.API
Exposed two ways (mirrors observer/uploader/consumer):
client.license.mintLicenseToken({ licensorIpId, licenseTermsId, amount?, receiver?, autoWrap?, autoApprove? })mintLicenseToken({ publicClient, walletClient, ...same })for callers without aCDRClientReturns
{ licenseTokenIds, feePaid, wrappedWei, txHashes }—txHashes.deposit/.approveare absent when those steps were skipped.Design notes
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).maxUint256approval by default so repeat mints skip the approve tx (opt out withautoApprove: false).autoWrap: falselikewise turns a shortfall into a typed error instead of wrapping.maxMintingFeeso terms repriced between prediction and execution revert rather than overpay.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.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 makeaccessCDRpoint at it when a license-gated read fails.)@story-protocol/core-sdkdeliberately avoided — sequential txs keep the new external surface to the addresses above.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 > 1with full mint-arg assertion, missing mint event, reverted deposit/mint (step-labeled error, no cascade), standalone function, wallet-required getter.dx-improvements.test.ts): runs the fullLicenseReadConditionjourney (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 viapredictMintingLicenseFee). Free asserts the wrap/approve skip path; fee-bearing asserts wrap→approve→pay (feePaid > 0,wrappedWei === feePaid, deposit+approve ran).Test plan
pnpm -r build,pnpm lint, unit tests green (270 sdk + 22 crypto + 21 monitor + 15 cli)Review fixes (jinn-agent)
license-contractsre-export fromindex.ts— raw ABIs/addresses stay out of the public semver surface.amount < 1or> Number.MAX_SAFE_INTEGER(guards theNumber(amount)token-id expansion).license.wrap.preflight.skippedwhengetBalanceis absent, for parity withConsumer.preflightBalance.feePaid > 0falsely 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.