@artblocks/abx-sdk 0.1.0-alpha.13 → 0.1.0-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/abi/generated.d.ts +926 -249
  2. package/dist/abi/generated.d.ts.map +1 -1
  3. package/dist/abi/generated.js +41 -39
  4. package/dist/abi/generated.js.map +1 -1
  5. package/dist/abi/index.d.ts +468 -61
  6. package/dist/abi/index.d.ts.map +1 -1
  7. package/dist/abi/index.js +16 -7
  8. package/dist/abi/index.js.map +1 -1
  9. package/dist/anchors.d.ts +70 -32
  10. package/dist/anchors.d.ts.map +1 -1
  11. package/dist/anchors.js +163 -32
  12. package/dist/anchors.js.map +1 -1
  13. package/dist/create2.d.ts +78 -7
  14. package/dist/create2.d.ts.map +1 -1
  15. package/dist/create2.js +149 -20
  16. package/dist/create2.js.map +1 -1
  17. package/dist/creator-token.d.ts +71 -4
  18. package/dist/creator-token.d.ts.map +1 -1
  19. package/dist/creator-token.js +94 -4
  20. package/dist/creator-token.js.map +1 -1
  21. package/dist/deploy.d.ts +49 -23
  22. package/dist/deploy.d.ts.map +1 -1
  23. package/dist/deploy.js +156 -76
  24. package/dist/deploy.js.map +1 -1
  25. package/dist/deployments.d.ts +4 -0
  26. package/dist/deployments.d.ts.map +1 -1
  27. package/dist/deployments.js +110 -45
  28. package/dist/deployments.js.map +1 -1
  29. package/dist/errors.d.ts +19 -0
  30. package/dist/errors.d.ts.map +1 -1
  31. package/dist/errors.js +24 -0
  32. package/dist/errors.js.map +1 -1
  33. package/dist/generator-document.d.ts +3 -2
  34. package/dist/generator-document.d.ts.map +1 -1
  35. package/dist/generator-document.js +3 -2
  36. package/dist/generator-document.js.map +1 -1
  37. package/dist/generator.d.ts +43 -1
  38. package/dist/generator.d.ts.map +1 -1
  39. package/dist/generator.js +29 -0
  40. package/dist/generator.js.map +1 -1
  41. package/dist/inspect.d.ts +1 -1
  42. package/dist/inspect.js +2 -2
  43. package/dist/inspect.js.map +1 -1
  44. package/dist/onchain-uri.d.ts +11 -0
  45. package/dist/onchain-uri.d.ts.map +1 -1
  46. package/dist/onchain-uri.js +20 -0
  47. package/dist/onchain-uri.js.map +1 -1
  48. package/dist/ops.d.ts +210 -11
  49. package/dist/ops.d.ts.map +1 -1
  50. package/dist/ops.js +308 -34
  51. package/dist/ops.js.map +1 -1
  52. package/dist/reconstruct.d.ts +1 -0
  53. package/dist/reconstruct.d.ts.map +1 -1
  54. package/dist/reconstruct.js +13 -1
  55. package/dist/reconstruct.js.map +1 -1
  56. package/dist/service.d.ts +1 -1
  57. package/dist/service.js +1 -1
  58. package/dist/spine.d.ts +3 -4
  59. package/dist/spine.d.ts.map +1 -1
  60. package/dist/spine.js +0 -0
  61. package/dist/spine.js.map +1 -1
  62. package/dist/staging.d.ts +32 -3
  63. package/dist/staging.d.ts.map +1 -1
  64. package/dist/staging.js +85 -8
  65. package/dist/staging.js.map +1 -1
  66. package/dist/tokendata.d.ts +1 -1
  67. package/dist/tokendata.d.ts.map +1 -1
  68. package/dist/tokendata.js +21 -3
  69. package/dist/tokendata.js.map +1 -1
  70. package/dist/tokens.d.ts +1 -1
  71. package/dist/tokens.js +1 -1
  72. package/dist/tokens.js.map +1 -1
  73. package/dist/types.d.ts +8 -1
  74. package/dist/types.d.ts.map +1 -1
  75. package/package.json +1 -1
package/dist/ops.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { encodeFunctionData, parseEventLogs, toHex, zeroAddress } from 'viem';
2
- import { oneOfOneImageAbi, oneOfOneImageFactoryAbi, oneOfOneImageFactoryBytecode, seriesImageAbi, seriesImageFactoryAbi, seriesImageFactoryBytecode, abxMetadataRendererBytecode, abxFixedPriceMinterAbi, abxFixedPriceMinterBytecode, abxFixedPriceMinter1155Abi, abxFixedPriceMinter1155Bytecode, abxSeedSourceBytecode, seriesCodeAbi, seriesCodeFactoryAbi, oneOfOneEditionAbi, oneOfOneEditionFactoryAbi, oneOfOneEditionFactoryBytecode, editionImageFactoryAbi, editionImageFactoryBytecode, editionCodeFactoryAbi, } from './abi/index.js';
2
+ import { oneOfOneImageAbi, oneOfOneImageFactoryAbi, seriesImageAbi, seriesImageFactoryAbi, abxMetadataRendererBytecode, abxFixedPriceMinterAbi, abxFixedPriceMinterBytecode, abxFixedPriceMinter1155Abi, abxFixedPriceMinter1155Bytecode, abxSeedSourceAbi, abxSeedSourceBytecode, seriesCodeAbi, seriesCodeFactoryAbi, oneOfOneEditionAbi, oneOfOneEditionFactoryAbi, editionImageFactoryAbi, editionCodeFactoryAbi, } from './abi/index.js';
3
3
  import { encodeTag } from './spine.js';
4
- import { ABX_SALT, CREATE2_PROXY, create2Calldata } from './create2.js';
4
+ import { SeedSourceUnusableError } from './errors.js';
5
+ // The two edition-image factories are library-linked, so their initcode comes from the `link*`
6
+ // helpers, never from the raw `*Bytecode` exports (which still carry solc's `__$…$__` placeholder).
7
+ import { ABX_SALT, CREATE2_PROXY, create2Calldata, linkEditionFactory, linkFactory, linkOneOfOneEditionFactory, linkSeriesFactory, } from './create2.js';
5
8
  const ZERO_VALUE = '0x0';
6
9
  function short(addr) {
7
10
  return addr.length > 12 ? `${addr.slice(0, 6)}…${addr.slice(-4)}` : addr;
@@ -470,12 +473,16 @@ export function prepareDeployOneOfOne(args) {
470
473
  },
471
474
  };
472
475
  }
473
- /** The unsigned form of the factory (trust-anchor) deploy — a contract creation. */
476
+ /** The unsigned form of the factory (trust-anchor) deploy — CREATE2 via the keyless proxy at the
477
+ * canonical salt, matching the forge `Deploy` script and `predictFactory()` (create2.ts). A trust
478
+ * anchor is the one address platforms allowlist, so it has to be the same on every chain and
479
+ * computable before the first tx; a plain creation (`to: null`) would make it depend on the
480
+ * deployer's nonce and could never match the recorded manifest. */
474
481
  export function prepareDeployFactory(args) {
475
482
  return {
476
483
  op: 'deploy-factory',
477
- to: null,
478
- data: oneOfOneImageFactoryBytecode,
484
+ to: CREATE2_PROXY,
485
+ data: create2Calldata(ABX_SALT.oneOfOneFactory, linkFactory()),
479
486
  value: ZERO_VALUE,
480
487
  chainId: args.chainId,
481
488
  summary: 'Deploy the canonical clone factory (trust anchor)',
@@ -610,12 +617,14 @@ export function prepareDeploySeries(args) {
610
617
  },
611
618
  };
612
619
  }
613
- /** The unsigned form of the Series factory (trust-anchor) deploy — a contract creation. */
620
+ /** The unsigned form of the Series factory (trust-anchor) deploy — CREATE2 via the keyless proxy at
621
+ * the canonical salt, matching the forge `DeploySeries` script and `predictSeriesFactory()`. Same
622
+ * reasoning as `prepareDeployFactory` above. */
614
623
  export function prepareDeploySeriesFactory(args) {
615
624
  return {
616
625
  op: 'deploy-series-factory',
617
- to: null,
618
- data: seriesImageFactoryBytecode,
626
+ to: CREATE2_PROXY,
627
+ data: create2Calldata(ABX_SALT.seriesFactory, linkSeriesFactory()),
619
628
  value: ZERO_VALUE,
620
629
  chainId: args.chainId,
621
630
  summary: 'Deploy the canonical Series clone factory (trust anchor)',
@@ -625,7 +634,7 @@ export function prepareDeploySeriesFactory(args) {
625
634
  // ── ERC-1155 editions (IAbxEditionMint + Edition Supply) ──────────────────────
626
635
  // Ids are caller-named, not a sequential cursor — a minter/owner targets any id, in any order,
627
636
  // with a per-mint amount. `mint`'s signature is uniform across all three edition kinds
628
- // (`OneOfOneEdition` reverts unless `id === 0`, its id space fixed to the single artwork), so
637
+ // (`OneOfOneEdition` reverts unless `id === 0`, its id space fixed to the single work), so
629
638
  // every op below encodes against `oneOfOneEditionAbi` — the narrowest-common-superset ABI for
630
639
  // the family, exactly like `oneOfOneImageAbi` plays that role on the 721 side.
631
640
  /**
@@ -734,15 +743,21 @@ export function prepareDeployOneOfOneEdition(args) {
734
743
  /** The unsigned form of the 1/1-edition factory (trust-anchor) deploy — CREATE2 via the keyless
735
744
  * proxy + canonical salt, so it lands at the SAME (predictable) address on every chain, matching
736
745
  * `predictOneOfOneEditionFactory()` (create2.ts). See {@link prepareDeployRenderer} for why this
737
- * is `to: CREATE2_PROXY`, not a plain creation. */
746
+ * is `to: CREATE2_PROXY`, not a plain creation.
747
+ *
748
+ * The initcode goes out LINKED against `AbxEditionLib` (`linkOneOfOneEditionFactory()`): the
749
+ * embedded `OneOfOneEdition` implementation delegates its uri / creator-token / edition-supply
750
+ * bodies into that library, so the shipped bytecode carries a solc `__$…$__` placeholder. That
751
+ * library must already be on-chain at `predictEditionLib()` for the deployed factory to work —
752
+ * {@link deployOneOfOneEditionFactory} (deploy.ts) is the path that guarantees it. */
738
753
  export function prepareDeployOneOfOneEditionFactory(args) {
739
754
  return {
740
755
  op: 'deploy-one-of-one-edition-factory',
741
756
  to: CREATE2_PROXY,
742
- data: create2Calldata(ABX_SALT.oneOfOneEditionFactory, oneOfOneEditionFactoryBytecode),
757
+ data: create2Calldata(ABX_SALT.oneOfOneEditionFactory, linkOneOfOneEditionFactory()),
743
758
  value: ZERO_VALUE,
744
759
  chainId: args.chainId,
745
- summary: 'Deploy the canonical 1/1-edition clone factory (trust anchor) — CREATE2, deterministic address',
760
+ summary: 'Deploy the canonical 1/1-edition clone factory (trust anchor), linked against AbxEditionLib — CREATE2, deterministic address',
746
761
  fields: {},
747
762
  };
748
763
  }
@@ -772,15 +787,17 @@ export function prepareDeployEditionImage(args) {
772
787
  }
773
788
  /** The unsigned form of the edition-image factory (trust-anchor) deploy — CREATE2 via the
774
789
  * keyless proxy + canonical salt, matching `predictEditionFactory()` (create2.ts). See {@link
775
- * prepareDeployRenderer} for why this is `to: CREATE2_PROXY`, not a plain creation. */
790
+ * prepareDeployRenderer} for why this is `to: CREATE2_PROXY`, not a plain creation, and {@link
791
+ * prepareDeployOneOfOneEditionFactory} for why the initcode is linked against `AbxEditionLib`
792
+ * (`EditionImage` delegates the same three mixins' bodies into it). */
776
793
  export function prepareDeployEditionFactory(args) {
777
794
  return {
778
795
  op: 'deploy-edition-factory',
779
796
  to: CREATE2_PROXY,
780
- data: create2Calldata(ABX_SALT.editionFactory, editionImageFactoryBytecode),
797
+ data: create2Calldata(ABX_SALT.editionFactory, linkEditionFactory()),
781
798
  value: ZERO_VALUE,
782
799
  chainId: args.chainId,
783
- summary: 'Deploy the canonical multi-artwork edition clone factory (trust anchor) — CREATE2, deterministic address',
800
+ summary: 'Deploy the canonical multi-work edition clone factory (trust anchor), linked against AbxEditionLib — CREATE2, deterministic address',
784
801
  fields: {},
785
802
  };
786
803
  }
@@ -846,25 +863,68 @@ export function prepareConfigureSale(args) {
846
863
  }
847
864
  /**
848
865
  * Buy one token from the shared minter. `to` omitted → minted to the signer (`purchase`);
849
- * `to` set → minted to that address (`purchaseTo`). `value` is the ETH to attach (equal to
850
- * the sale price for an ETH sale; `0` for an ERC-20 sale, where the buyer must have approved
851
- * the minter to pull `price` beforehand). Public — no owner rights needed.
866
+ * `to` set → minted to that address (`purchaseTo`). Public — no owner rights needed.
867
+ *
868
+ * `sale` is the live terms ({@link readSaleConfig}), and they are **mandatory**: the minter takes
869
+ * `(expectedPaymentToken, maxPrice)` and reverts `SaleTermsChanged` if the sale has moved, which is
870
+ * what stops a project owner from front-running a pending purchase and spending the buyer's whole
871
+ * ERC-20 allowance (`configure` has no timelock). The ETH to attach is derived from those terms —
872
+ * `price` for an ETH sale, `0` for an ERC-20 one (where the buyer must have approved the minter to
873
+ * pull `price` beforehand) — so a caller can't desync the payment from the guard.
874
+ *
875
+ * `maxPrice` defaults to `sale.price` (accept exactly what was read). Pass it to state a wider
876
+ * ceiling. It bounds the guard only; the ETH attached still comes from `sale.price`, since the minter
877
+ * wants exact payment. There is deliberately no "no maximum" sentinel.
878
+ *
879
+ * **A wide `maxPrice` is how a caller opts out of the protection.** The minter refuses a sentinel
880
+ * precisely so that a bound has to be stated, but nothing stops a caller from stating an enormous
881
+ * one — and on the ERC-20 lane that hands back exactly the vector the guard closes: the minter pulls
882
+ * the live price from the buyer's standing allowance, so a ceiling of `2^256-1` lets a re-priced sale
883
+ * take the whole allowance. Omitting the argument is the safe path and is what every caller in this
884
+ * repo does. If you widen it, widen it by an amount you would be content to lose, and never derive it
885
+ * from a price you read in the same breath as sending — that is the same thing as no bound at all.
886
+ *
887
+ * **On the ETH lane, `maxPrice` is a terms ASSERTION, not slippage tolerance — do not sell it as
888
+ * one.** The minter requires `msg.value == price` (an equality) and V1 has no refund path, so an
889
+ * in-flight ETH purchase reverts `WrongPayment` if the price moves in *either* direction, however
890
+ * wide the ceiling. Raising `maxPrice` on an ETH sale buys the buyer nothing at all. Real tolerance
891
+ * exists only on the **ERC-20** lane, where the minter pulls the live `price` from the buyer's
892
+ * allowance and any price at or below the ceiling settles. Re-pricing a live ETH sale therefore fails
893
+ * every buy already in flight, by design — pause first for a clean cutover.
852
894
  */
853
895
  export function preparePurchase(args) {
854
- const value = BigInt(args.value ?? 0n);
896
+ const paymentToken = args.sale.paymentToken;
897
+ const price = BigInt(args.sale.price);
898
+ const isEth = paymentToken === zeroAddress;
899
+ const maxPrice = args.maxPrice === undefined ? price : BigInt(args.maxPrice);
900
+ const value = isEth ? price : 0n; // exact payment; an ERC-20 sale attaches none
855
901
  const toRecipient = args.to && args.to !== zeroAddress;
902
+ const cost = isEth ? `${price} wei` : `${price} units of ${short(paymentToken)}`;
856
903
  return {
857
904
  op: 'purchase',
858
905
  to: args.minter,
859
906
  data: toRecipient
860
- ? encodeFunctionData({ abi: abxFixedPriceMinterAbi, functionName: 'purchaseTo', args: [args.token, args.to] })
861
- : encodeFunctionData({ abi: abxFixedPriceMinterAbi, functionName: 'purchase', args: [args.token] }),
907
+ ? encodeFunctionData({
908
+ abi: abxFixedPriceMinterAbi,
909
+ functionName: 'purchaseTo',
910
+ args: [args.token, args.to, paymentToken, maxPrice],
911
+ })
912
+ : encodeFunctionData({
913
+ abi: abxFixedPriceMinterAbi,
914
+ functionName: 'purchase',
915
+ args: [args.token, paymentToken, maxPrice],
916
+ }),
862
917
  value: value > 0n ? toHex(value) : ZERO_VALUE,
863
918
  chainId: args.chainId,
864
- summary: toRecipient
865
- ? `Buy 1 token → ${short(args.to)}${value > 0n ? ` for ${value} wei` : ''}`
866
- : `Buy 1 token${value > 0n ? ` for ${value} wei` : ''}`,
867
- fields: { minter: args.minter, token: args.token, to: args.to ?? '(sender)', value: value.toString() },
919
+ summary: toRecipient ? `Buy 1 token → ${short(args.to)} for ${cost}` : `Buy 1 token for ${cost}`,
920
+ fields: {
921
+ minter: args.minter,
922
+ token: args.token,
923
+ to: args.to ?? '(sender)',
924
+ value: value.toString(),
925
+ expectedPaymentToken: paymentToken,
926
+ maxPrice: maxPrice.toString(),
927
+ },
868
928
  };
869
929
  }
870
930
  export async function readSaleConfig(client, minter, token) {
@@ -892,7 +952,7 @@ export function prepareDeployFixedPriceMinter(args) {
892
952
  }
893
953
  // ── Fixed-price EDITION minter (the Minter spine's 1155 lane) ────────────────
894
954
  // The structural sibling of the block above, keyed one step finer: `(token, id)` instead of just
895
- // `token`, since an edition project prices each artwork on its own terms. See
955
+ // `token`, since an edition project prices each work on its own terms. See
896
956
  // `AbxFixedPriceMinter1155`'s own doc comment for the two-grant model (mint rights via
897
957
  // `setMinter`, sale terms via `configure` — unchanged from the 721 lane, one id finer).
898
958
  /**
@@ -930,23 +990,44 @@ export function prepareConfigureSale1155(args) {
930
990
  * Buy `quantity` copies of `id` from the shared edition minter. The id/quantity-keyed twin of
931
991
  * {@link preparePurchase}. `to` omitted → minted to the signer (`purchase`); `to` set → minted to
932
992
  * that address (`purchaseTo`). Public — no owner rights needed.
993
+ *
994
+ * `sale` is the live terms ({@link readSaleConfig1155}) and is mandatory, same terms guard as the 721
995
+ * lane — except the minter bounds the **total** (`price × quantity`), which is what actually leaves
996
+ * the buyer's balance. `price × quantity` is computed here (once), for both the attached ETH and the
997
+ * bound; `maxTotalPrice` defaults to it, and passing it states a wider ceiling.
998
+ *
999
+ * The ETH-lane caveat on {@link preparePurchase} applies here identically: `msg.value` must equal the
1000
+ * total exactly, so `maxTotalPrice` is an assertion about the terms, **not** slippage tolerance. Only
1001
+ * an ERC-20 edition sale gets real tolerance.
933
1002
  */
934
1003
  export function preparePurchase1155(args) {
935
1004
  const id = BigInt(args.tokenId);
936
1005
  const qty = BigInt(args.quantity);
937
- const value = BigInt(args.value ?? 0n);
1006
+ const paymentToken = args.sale.paymentToken;
1007
+ const total = BigInt(args.sale.price) * qty;
1008
+ const isEth = paymentToken === zeroAddress;
1009
+ const maxTotalPrice = args.maxTotalPrice === undefined ? total : BigInt(args.maxTotalPrice);
1010
+ const value = isEth ? total : 0n; // exact payment; an ERC-20 sale attaches none
938
1011
  const toRecipient = args.to && args.to !== zeroAddress;
1012
+ const cost = isEth ? `${total} wei` : `${total} units of ${short(paymentToken)}`;
1013
+ const copies = `${qty} cop${qty === 1n ? 'y' : 'ies'} of #${id}`;
939
1014
  return {
940
1015
  op: 'purchase',
941
1016
  to: args.minter,
942
1017
  data: toRecipient
943
- ? encodeFunctionData({ abi: abxFixedPriceMinter1155Abi, functionName: 'purchaseTo', args: [args.token, id, qty, args.to] })
944
- : encodeFunctionData({ abi: abxFixedPriceMinter1155Abi, functionName: 'purchase', args: [args.token, id, qty] }),
1018
+ ? encodeFunctionData({
1019
+ abi: abxFixedPriceMinter1155Abi,
1020
+ functionName: 'purchaseTo',
1021
+ args: [args.token, id, qty, args.to, paymentToken, maxTotalPrice],
1022
+ })
1023
+ : encodeFunctionData({
1024
+ abi: abxFixedPriceMinter1155Abi,
1025
+ functionName: 'purchase',
1026
+ args: [args.token, id, qty, paymentToken, maxTotalPrice],
1027
+ }),
945
1028
  value: value > 0n ? toHex(value) : ZERO_VALUE,
946
1029
  chainId: args.chainId,
947
- summary: toRecipient
948
- ? `Buy ${qty} cop${qty === 1n ? 'y' : 'ies'} of #${id} → ${short(args.to)}${value > 0n ? ` for ${value} wei` : ''}`
949
- : `Buy ${qty} cop${qty === 1n ? 'y' : 'ies'} of #${id}${value > 0n ? ` for ${value} wei` : ''}`,
1030
+ summary: toRecipient ? `Buy ${copies} → ${short(args.to)} for ${cost}` : `Buy ${copies} for ${cost}`,
950
1031
  fields: {
951
1032
  minter: args.minter,
952
1033
  token: args.token,
@@ -954,6 +1035,8 @@ export function preparePurchase1155(args) {
954
1035
  quantity: qty.toString(),
955
1036
  to: args.to ?? '(sender)',
956
1037
  value: value.toString(),
1038
+ expectedPaymentToken: paymentToken,
1039
+ maxTotalPrice: maxTotalPrice.toString(),
957
1040
  },
958
1041
  };
959
1042
  }
@@ -995,6 +1078,118 @@ export function prepareDeploySeedSource(args) {
995
1078
  fields: {},
996
1079
  };
997
1080
  }
1081
+ // ── the seed source (Seed Source extension, Register 2) ───────────────────────
1082
+ // Mint-time randomness is CONFIGURATION, not hard-coded token logic: the token holds one address
1083
+ // and calls `IAbxSeedSource.seed(tokenId, to)` on it at mint. The canonical `AbxSeedSource` is
1084
+ // deliberately pseudorandom (replayable after the fact, computable *during* the mint tx), so the
1085
+ // documented escape for anything lottery-like is to point this at a commit-reveal / VRF-backed
1086
+ // source of the project's own. That promise is only real if a creator can actually SET the address
1087
+ // — which is what the two ops below, plus the probe, exist for.
1088
+ /**
1089
+ * Set (or clear, with `0x0`) the seed source a code project draws mint seeds from. Signer must be
1090
+ * the owner. **Future mints only** — a seed is settled once assigned, so re-pointing never rewrites
1091
+ * a token that already has one.
1092
+ *
1093
+ * Pass an address that {@link probeSeedSource} says is usable. The chain does NOT validate the
1094
+ * source (`SeedSourceExtension.setSeedSource` stores whatever it's handed), and the failure mode of
1095
+ * a bad one is the worst kind: `seedSource()` reads back exactly what you set, the `SeedSourceSet`
1096
+ * event fires, everything looks configured — and then every mint reverts in the ABI decode of the
1097
+ * source's (empty or short) return. Callers should probe first and refuse before gas.
1098
+ */
1099
+ export function prepareSetSeedSource(args) {
1100
+ const clearing = args.seedSource === zeroAddress;
1101
+ return {
1102
+ op: 'set-seed-source',
1103
+ to: args.contract,
1104
+ data: encodeFunctionData({ abi: seriesCodeAbi, functionName: 'setSeedSource', args: [args.seedSource] }),
1105
+ value: ZERO_VALUE,
1106
+ chainId: args.chainId,
1107
+ summary: clearing
1108
+ ? 'Clear the seed source (future mints draw no seed; assigned seeds stay settled)'
1109
+ : `Set seed source → ${short(args.seedSource)} (future mints only)`,
1110
+ fields: { contract: args.contract, seedSource: args.seedSource },
1111
+ };
1112
+ }
1113
+ /**
1114
+ * The seed source a token currently draws from — `zeroAddress` for "no mint-time seed", and
1115
+ * `undefined` when the getter isn't there at all (a 1/1 or an image Series never composed the
1116
+ * extension). Those two are NOT the same fact and must not be collapsed: one is a code project
1117
+ * that opted out, the other is a token that has no such knob to set.
1118
+ */
1119
+ export async function readSeedSource(client, contract) {
1120
+ return (await client
1121
+ .readContract({ address: contract, abi: seriesCodeAbi, functionName: 'seedSource' })
1122
+ .catch(() => undefined));
1123
+ }
1124
+ /**
1125
+ * Ask a candidate seed source the exact question the token will ask it, and report whether it
1126
+ * answers with 32 bytes.
1127
+ *
1128
+ * **`code.length > 0` is not enough**, which is the whole reason this exists. Three separate shapes
1129
+ * pass a has-code check, read back correctly from `seedSource()`, emit `SeedSourceSet`, and then
1130
+ * revert **every mint of the collection**: no code, a permissive fallback that returns empty (a
1131
+ * Safe, an uninitialised proxy, a 7702-delegated EOA — the same footgun the transfer-validator
1132
+ * probe in `CreatorToken.sol` was written for), and anything answering with fewer than 32 bytes.
1133
+ * A misconfigured seed source is silent until the first buyer, so the check belongs at configure
1134
+ * time, loudly.
1135
+ *
1136
+ * `IAbxSeedSource.seed` is **non-`view`** (sources may keep state — commit-reveal, oracle-fed), but
1137
+ * an `eth_call` simulates a state-changing function perfectly well and the canonical implementation
1138
+ * is itself `view`, so one `eth_call` probes both kinds without sending anything.
1139
+ *
1140
+ * The call is made **as the token** (`from` = `opts.as`, when given): `msg.sender` namespaces the
1141
+ * canonical seed and is exactly what a caller-gating custom source checks, so probing from a
1142
+ * random address would false-negative a legitimate source. Same reason the transfer-validator
1143
+ * research used `cast call --from <collection>`.
1144
+ *
1145
+ * Never throws for an on-chain reason — every failure is a verdict.
1146
+ */
1147
+ export async function probeSeedSource(client, source, opts = {}) {
1148
+ let code;
1149
+ try {
1150
+ code = await client.getCode({ address: source });
1151
+ }
1152
+ catch (err) {
1153
+ return { verdict: 'unreachable', address: source, error: firstLine(err) };
1154
+ }
1155
+ if (!code || code === '0x')
1156
+ return { verdict: 'no-code', address: source };
1157
+ const tokenId = opts.tokenId ?? 0n;
1158
+ // `to` mirrors the mint: the token asks for a seed for a recipient. The canonical source ignores
1159
+ // it; a custom one may not, so pass a plausible non-zero address rather than 0x0 (which a source
1160
+ // validating its recipient would rightly refuse, turning a good source into a false negative).
1161
+ const to = opts.as ?? source;
1162
+ try {
1163
+ const res = await client.call({
1164
+ to: source,
1165
+ data: encodeFunctionData({ abi: abxSeedSourceAbi, functionName: 'seed', args: [tokenId, to] }),
1166
+ ...(opts.as ? { account: opts.as } : {}),
1167
+ });
1168
+ const bytes = res.data ? (res.data.length - 2) / 2 : 0;
1169
+ if (bytes === 0)
1170
+ return { verdict: 'empty-return', address: source, returnedBytes: 0 };
1171
+ if (bytes < 32)
1172
+ return { verdict: 'short-return', address: source, returnedBytes: bytes };
1173
+ return { verdict: 'ok', address: source, returnedBytes: bytes };
1174
+ }
1175
+ catch (err) {
1176
+ return { verdict: 'reverted', address: source, error: firstLine(err) };
1177
+ }
1178
+ }
1179
+ /** First line of an error message — a revert reason is readable; viem's full dump is not. */
1180
+ function firstLine(err) {
1181
+ return String(err?.message ?? err).split('\n')[0].trim();
1182
+ }
1183
+ /**
1184
+ * {@link probeSeedSource}, as a guard: throws {@link SeedSourceUnusableError} unless the verdict is
1185
+ * `ok`. For an SDK caller that just wants "refuse a bad source" without composing prose; the CLI
1186
+ * uses the probe directly so it can name its own flags in the refusal.
1187
+ */
1188
+ export async function assertSeedSourceUsable(client, source, opts = {}) {
1189
+ const probe = await probeSeedSource(client, source, opts);
1190
+ if (probe.verdict !== 'ok')
1191
+ throw new SeedSourceUnusableError(probe);
1192
+ }
998
1193
  /** Deterministic deploy of a {SeriesCode} clone — the code-project factory op. */
999
1194
  export function prepareDeploySeriesCode(args) {
1000
1195
  return {
@@ -1129,6 +1324,23 @@ export function prepareLockDependencies(args) {
1129
1324
  fields: { contract: args.contract },
1130
1325
  };
1131
1326
  }
1327
+ /** Freeze the on-chain program (script chunks) forever — `setScriptChunk`/`removeLastScriptChunk`
1328
+ * revert after this. Irreversible, owner-only. This is the lock that actually freezes the WORK
1329
+ * of a code project; `lock-field`/`lock-uri` only freeze the metadata, and `lock-dependencies` only
1330
+ * the library set. All of them together freeze everything the CONTRACT stores — which is not the
1331
+ * same as a frozen output: params have no lock and the renderer serves them, and a `Registry`
1332
+ * dependency resolves from the registry at read time. */
1333
+ export function prepareLockScript(args) {
1334
+ return {
1335
+ op: 'lock-script',
1336
+ to: args.contract,
1337
+ data: encodeFunctionData({ abi: seriesCodeAbi, functionName: 'lockScript', args: [] }),
1338
+ value: ZERO_VALUE,
1339
+ chainId: args.chainId,
1340
+ summary: 'Freeze the on-chain program (script chunks) — permanent',
1341
+ fields: { contract: args.contract },
1342
+ };
1343
+ }
1132
1344
  /** Set a CONTRACT-scope param to a literal `bytes32` (the raw owner setter — Params base).
1133
1345
  * Only for schema-less keys (a schema'd key closes the raw path: `SchemaGoverned`). This is
1134
1346
  * the write behind well-known contract params like `display.gateway`. The key joins the
@@ -1169,7 +1381,15 @@ export function prepareSetContractParamData(args) {
1169
1381
  /** Wire the three param-lifecycle hook addresses (configure/augment/transfer) in ONE owner call.
1170
1382
  * The contract has no per-hook setter — `setParamHooks` writes all three at once — so a caller
1171
1383
  * changing one hook must pass the CURRENT values for the other two (read them via `paramHooks()`),
1172
- * else it silently clears them. `zeroAddress` for any role means "no hook". SeriesCode only. */
1384
+ * else it silently clears them. `zeroAddress` for any role means "no hook". SeriesCode/EditionCode
1385
+ * only.
1386
+ *
1387
+ * Note what the `transferHook` slot is: a **veto**. Its revert bubbles and fails the transfer, and
1388
+ * a mint is a transfer from `0x0`, so a reverting hook stops minting for the project too. Setting
1389
+ * one is therefore a standing power over a collector's ability to sell — disclose it, and offer
1390
+ * {@link prepareLockParamHooks} to a project that wants to prove it will never arm one.
1391
+ *
1392
+ * Reverts `ParamHooksLocked()` once {@link prepareLockParamHooks} has been sent. */
1173
1393
  export function prepareSetParamHooks(args) {
1174
1394
  return {
1175
1395
  op: 'set-param-hooks',
@@ -1190,6 +1410,60 @@ export function prepareSetParamHooks(args) {
1190
1410
  },
1191
1411
  };
1192
1412
  }
1413
+ /**
1414
+ * Is the param hook set frozen (`lockParamHooks()` already sent)?
1415
+ *
1416
+ * One `eth_call` to `paramHooksLocked()`. This used to simulate `setParamHooks` as the owner and
1417
+ * watch for a `ParamHooksLocked()` revert, on the stated grounds that no getter existed — it does
1418
+ * now (`IAbxConfigurableParams`), and the simulation cost two extra reads (`paramHooks` + `owner`)
1419
+ * to infer what one read states.
1420
+ *
1421
+ * The other honest read is the `ParamHooksFrozen` event, which is what `reconstructProject` folds
1422
+ * (`ProjectState.paramHooks.locked`). Prefer the fold when you already have the logs; use this when
1423
+ * you have an address and one RPC round trip, which is `abx state`'s situation.
1424
+ *
1425
+ * Returns `undefined` — never throws for an on-chain reason — when the answer can't be established:
1426
+ * not a ConfigurableParams project, or the node refused the call. A caller must render that as
1427
+ * "unknown", never as "unlocked".
1428
+ */
1429
+ export async function readParamHooksLocked(client, contract) {
1430
+ try {
1431
+ return (await client.readContract({
1432
+ address: contract,
1433
+ abi: seriesCodeAbi,
1434
+ functionName: 'paramHooksLocked',
1435
+ }));
1436
+ }
1437
+ catch {
1438
+ return undefined;
1439
+ }
1440
+ }
1441
+ /**
1442
+ * Freeze the param-lifecycle hook set forever — `setParamHooks` reverts `ParamHooksLocked()` after
1443
+ * this. Irreversible, owner-only. The sibling of `lock-script`/`lock-dependencies`/`lock-uri`, and
1444
+ * the one aimed at a **buyer** rather than at the metadata: the `transferHook` is a veto (its revert
1445
+ * fails a transfer, and a mint is a transfer from `0x0`), so while the hooks are unwritten-in-stone a
1446
+ * project retains a standing power over whether a collector can ever sell. Sending this gives up
1447
+ * three abilities permanently — arming a transfer veto, arming a write-time configure veto, and
1448
+ * re-pointing or clearing the read-time augment hook — and it cannot be undone, re-opened, or
1449
+ * time-limited. A project that never wants the power sends it before the sale; a buyer reads
1450
+ * `paramHooks()` plus the `ParamHooksFrozen` event (`abx state`) to check.
1451
+ *
1452
+ * It does NOT freeze anything else: schemas, values, script, dependencies, and URIs all keep their
1453
+ * own locks, and a hook already set keeps running exactly as before — freezing the SET is not
1454
+ * disarming the hooks in it.
1455
+ */
1456
+ export function prepareLockParamHooks(args) {
1457
+ return {
1458
+ op: 'lock-param-hooks',
1459
+ to: args.contract,
1460
+ data: encodeFunctionData({ abi: seriesCodeAbi, functionName: 'lockParamHooks', args: [] }),
1461
+ value: ZERO_VALUE,
1462
+ chainId: args.chainId,
1463
+ summary: 'Freeze the param hooks (configure/augment/transfer) — permanent',
1464
+ fields: { contract: args.contract },
1465
+ };
1466
+ }
1193
1467
  /** Set a schema-governed PostParam (literal scalar) — signer must satisfy the schema's auth. */
1194
1468
  export function prepareConfigureTokenParam(args) {
1195
1469
  return {
@@ -1284,7 +1558,7 @@ export async function readParamSchema(client, contract, key) {
1284
1558
  *
1285
1559
  * What retiring does NOT do: erase a value already stored. That value keeps being served in token
1286
1560
  * data. It cannot be removed, deliberately — a value written under a `TokenOwner` or `Address` leg
1287
- * came from a collector, and the artist should not be able to delete someone else's contribution.
1561
+ * came from a collector, and the creator should not be able to delete someone else's contribution.
1288
1562
  */
1289
1563
  export function prepareRetireParam(args) {
1290
1564
  const lockAfter = args.at ?? Math.floor(Date.now() / 1000) - 1;