ox 0.14.29 → 0.14.30

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.
@@ -74,7 +74,6 @@ export type GetType<
74
74
  ? 'keychain'
75
75
  : envelope extends {
76
76
  account: Address.Address
77
- genesisConfigId: `0x${string}`
78
77
  signatures: any
79
78
  }
80
79
  ? 'multisig'
@@ -150,9 +149,10 @@ export type KeychainRpc = {
150
149
  /**
151
150
  * Native multisig signature (type `0x05`).
152
151
  *
153
- * Wraps a set of primitive owner approvals (secp256k1, p256, or webAuthn) over the
154
- * multisig owner approval digest. The transaction sender is the derived `account`,
155
- * authorized once the recovered owner weights meet the configured threshold.
152
+ * Wraps a set of owner approvals (secp256k1, p256, webAuthn, or nested
153
+ * multisig) over the multisig owner approval digest. The transaction sender is
154
+ * the derived `account`, authorized once the recovered owner weights meet the
155
+ * configured threshold.
156
156
  *
157
157
  * [TIP-1061](https://tips.sh/1061)
158
158
  */
@@ -160,9 +160,11 @@ export type Multisig<bigintType = bigint, numberType = number> = {
160
160
  type: 'multisig'
161
161
  /** Native multisig account address. */
162
162
  account: Address.Address
163
- /** Permanent config ID derived from the initial multisig config. */
164
- genesisConfigId: Hex.Hex
165
- /** Primitive owner approvals over the multisig owner approval digest. */
163
+ /**
164
+ * Owner approvals over the multisig owner approval digest. Each approval is
165
+ * either a primitive signature or a nested multisig signature (keychain
166
+ * approvals are invalid).
167
+ */
166
168
  signatures: readonly SignatureEnvelope<bigintType, numberType>[]
167
169
  /**
168
170
  * Initial native multisig config for bootstrapping this account. Present only on
@@ -176,14 +178,8 @@ export type MultisigRpc = {
176
178
  type: 'multisig'
177
179
  account: Address.Address
178
180
  /**
179
- * The permanent multisig config ID (TIP-1061 wire field `config_id`).
180
- * Maps to `genesisConfigId` on the typed
181
- * {@link ox#SignatureEnvelope.Multisig} envelope.
182
- */
183
- configId: Hex.Hex
184
- /**
185
- * Encoded primitive owner approvals (raw serialized signatures), matching the
186
- * node's `Vec<Bytes>` representation.
181
+ * Encoded owner approvals (raw serialized signatures), matching the node's
182
+ * `Vec<Bytes>` representation.
187
183
  */
188
184
  signatures: readonly Serialized[]
189
185
  init?: MultisigConfig.Config | undefined
@@ -333,11 +329,24 @@ export function assert(envelope: PartialBy<SignatureEnvelope, 'type'>): void {
333
329
  const multisig = envelope as Multisig
334
330
  const missing: string[] = []
335
331
  if (!multisig.account) missing.push('account')
336
- if (!multisig.genesisConfigId) missing.push('genesisConfigId')
337
332
  if (!Array.isArray(multisig.signatures)) missing.push('signatures')
338
333
  if (missing.length > 0)
339
334
  throw new MissingPropertiesError({ envelope, missing, type: 'multisig' })
340
- for (const inner of multisig.signatures) assert(inner)
335
+ if (getNestingDepth(multisig) > MultisigConfig.maxNestingDepth)
336
+ throw new InvalidMultisigApprovalError({
337
+ reason: `multisig nesting depth exceeds ${MultisigConfig.maxNestingDepth}`,
338
+ })
339
+ for (const inner of multisig.signatures) {
340
+ if (inner.type === 'keychain')
341
+ throw new InvalidMultisigApprovalError({
342
+ reason: 'keychain owner approvals are not allowed',
343
+ })
344
+ if (inner.type === 'multisig' && inner.init)
345
+ throw new InvalidMultisigApprovalError({
346
+ reason: 'nested multisig owner approvals cannot carry `init`',
347
+ })
348
+ assert(inner)
349
+ }
341
350
  if (multisig.init) MultisigConfig.assert(multisig.init)
342
351
  return
343
352
  }
@@ -346,12 +355,22 @@ export function assert(envelope: PartialBy<SignatureEnvelope, 'type'>): void {
346
355
  export declare namespace assert {
347
356
  type ErrorType =
348
357
  | CoercionError
358
+ | InvalidMultisigApprovalError
349
359
  | MissingPropertiesError
350
360
  | MultisigConfig.assert.ErrorType
351
361
  | Signature.assert.ErrorType
352
362
  | Errors.GlobalErrorType
353
363
  }
354
364
 
365
+ // Returns the deepest multisig path length within a multisig envelope.
366
+ function getNestingDepth(envelope: Multisig): number {
367
+ let deepest = 0
368
+ for (const inner of envelope.signatures)
369
+ if (inner.type === 'multisig')
370
+ deepest = Math.max(deepest, getNestingDepth(inner))
371
+ return 1 + deepest
372
+ }
373
+
355
374
  /**
356
375
  * Extracts the address of the signer from a {@link ox#SignatureEnvelope.SignatureEnvelope}.
357
376
  *
@@ -618,24 +637,26 @@ export function deserialize(value: Serialized): SignatureEnvelope {
618
637
  }
619
638
 
620
639
  if (typeId === serializedMultisigType) {
621
- // Wire format: `0x05 || rlp([account, genesisConfigId, signatures, init])`. `init`
622
- // is optional: absent when the element is missing or the `0x80` placeholder
623
- // (decoded as the empty string `0x`), otherwise the bootstrap config list.
624
- const [account, genesisConfigId, signatures, init] = Rlp.toHex(data) as [
625
- Hex.Hex,
640
+ // Wire format: `0x05 || rlp([account, signatures, init?])`. `init` is a
641
+ // trailing optional: the bootstrap config list when present, otherwise
642
+ // omitted entirely (the empty `0x80` placeholder is rejected).
643
+ const [account, signatures, init] = Rlp.toHex(data) as [
626
644
  Hex.Hex,
627
645
  readonly Hex.Hex[],
628
646
  (Hex.Hex | MultisigConfig.Tuple)?,
629
647
  ]
648
+ if (typeof init !== 'undefined' && !Array.isArray(init))
649
+ throw new InvalidSerializedError({
650
+ reason:
651
+ 'Invalid multisig `init`: expected the bootstrap config list or an omitted trailing element',
652
+ serialized,
653
+ })
630
654
  return {
631
655
  type: 'multisig',
632
656
  account,
633
- genesisConfigId,
634
657
  signatures: signatures.map((signature) => deserialize(signature)),
635
- ...(init && init !== '0x'
636
- ? {
637
- init: MultisigConfig.fromTuple(init as MultisigConfig.Tuple),
638
- }
658
+ ...(init
659
+ ? { init: MultisigConfig.fromTuple(init as MultisigConfig.Tuple) }
639
660
  : {}),
640
661
  } satisfies Multisig
641
662
  }
@@ -761,9 +782,9 @@ export function deserialize(value: Serialized): SignatureEnvelope {
761
782
  * @example
762
783
  * ### Multisig (from genesis config)
763
784
  *
764
- * Pass `genesisConfig` to derive `account` and `genesisConfigId` automatically.
765
- * Set `init: true` to opt into bootstrap (uses `genesisConfig` as the
766
- * bootstrap `init`); omit `init` for subsequent (non-bootstrap) transactions.
785
+ * Pass `genesisConfig` to derive `account` automatically. Set `init: true` to
786
+ * opt into bootstrap (uses `genesisConfig` as the bootstrap `init`); omit
787
+ * `init` for subsequent (non-bootstrap) transactions.
767
788
  *
768
789
  * ```ts twoslash
769
790
  * import { Secp256k1 } from 'ox'
@@ -821,18 +842,12 @@ export function from<const value extends from.Value>(
821
842
  init?: MultisigConfig.Config | boolean | undefined
822
843
  }
823
844
  const { genesisConfig, init, ...rest } = multisig
824
- // Derive `account`/`genesisConfigId` from `genesisConfig` when not provided
825
- // explicitly.
845
+ // Derive `account` from `genesisConfig` when not provided explicitly.
826
846
  const account = (() => {
827
847
  if (rest.account) return rest.account
828
848
  if (genesisConfig) return MultisigConfig.getAddress(genesisConfig)
829
849
  return rest.account
830
850
  })()
831
- const genesisConfigId = (() => {
832
- if (rest.genesisConfigId) return rest.genesisConfigId
833
- if (genesisConfig) return MultisigConfig.toId(genesisConfig)
834
- return rest.genesisConfigId
835
- })()
836
851
  // `init: true` opts into bootstrap using the supplied `genesisConfig`.
837
852
  // Otherwise, `init` is treated as the explicit bootstrap config (or
838
853
  // omitted).
@@ -840,7 +855,6 @@ export function from<const value extends from.Value>(
840
855
  return {
841
856
  ...rest,
842
857
  account,
843
- genesisConfigId,
844
858
  signatures: rest.signatures.map((signature) => from(signature)),
845
859
  // Normalize the bootstrap config (sorts owners, defaults the salt) so the
846
860
  // in-memory envelope matches what `deserialize` reconstructs.
@@ -892,9 +906,9 @@ export declare namespace from {
892
906
  }
893
907
 
894
908
  /**
895
- * Multisig envelope input variant where `account` and `genesisConfigId` are derived
896
- * from the supplied `genesisConfig`. Pass `init: true` to opt into bootstrap
897
- * (uses `genesisConfig` as the bootstrap `init`); omit `init` for subsequent
909
+ * Multisig envelope input variant where `account` is derived from the
910
+ * supplied `genesisConfig`. Pass `init: true` to opt into bootstrap (uses
911
+ * `genesisConfig` as the bootstrap `init`); omit `init` for subsequent
898
912
  * (non-bootstrap) transactions.
899
913
  */
900
914
  type MultisigFromGenesisConfig = {
@@ -1035,17 +1049,12 @@ export function fromRpc(envelope: SignatureEnvelopeRpc): SignatureEnvelope {
1035
1049
 
1036
1050
  if (
1037
1051
  envelope.type === 'multisig' ||
1038
- ('account' in envelope &&
1039
- 'configId' in envelope &&
1040
- 'signatures' in envelope)
1052
+ ('account' in envelope && 'signatures' in envelope)
1041
1053
  ) {
1042
1054
  const multisig = envelope as MultisigRpc
1043
1055
  return {
1044
1056
  type: 'multisig',
1045
1057
  account: multisig.account,
1046
- // Map RPC wire field `configId` (TIP-1061 spec name) to the typed
1047
- // envelope's `genesisConfigId`.
1048
- genesisConfigId: multisig.configId,
1049
1058
  // Owner approvals are raw serialized signatures (node `Vec<Bytes>`).
1050
1059
  signatures: multisig.signatures.map((signature) =>
1051
1060
  deserialize(signature),
@@ -1132,8 +1141,7 @@ export function getType<
1132
1141
 
1133
1142
  // Detect Multisig signature
1134
1143
  if (
1135
- (('account' in envelope && 'genesisConfigId' in envelope) ||
1136
- 'genesisConfig' in envelope) &&
1144
+ ('account' in envelope || 'genesisConfig' in envelope) &&
1137
1145
  'signatures' in envelope
1138
1146
  )
1139
1147
  return 'multisig' as never
@@ -1233,17 +1241,15 @@ export function serialize(
1233
1241
 
1234
1242
  if (type === 'multisig') {
1235
1243
  const multisig = envelope as Multisig
1236
- // Format: `0x05 || rlp([account, genesisConfigId, signatures, init])`, where each
1237
- // owner approval is an encoded primitive signature. `init` is the bootstrap
1238
- // config (an RLP list) when present, otherwise the canonical empty-string
1239
- // placeholder (`0x` → RLP `0x80`).
1244
+ // Format: `0x05 || rlp([account, signatures, init?])`, where each owner
1245
+ // approval is an encoded signature. `init` is a trailing optional: the
1246
+ // bootstrap config list when present, otherwise omitted entirely.
1240
1247
  return Hex.concat(
1241
1248
  serializedMultisigType,
1242
1249
  Rlp.fromHex([
1243
1250
  multisig.account,
1244
- multisig.genesisConfigId,
1245
1251
  multisig.signatures.map((signature) => serialize(signature)),
1246
- multisig.init ? MultisigConfig.toTuple(multisig.init) : '0x',
1252
+ ...(multisig.init ? [MultisigConfig.toTuple(multisig.init)] : []),
1247
1253
  ]),
1248
1254
  options.magic ? magicBytes : '0x',
1249
1255
  )
@@ -1271,10 +1277,10 @@ export declare namespace serialize {
1271
1277
  * ({@link ox#MultisigConfig.(getSignPayload:function)}), so the signer of
1272
1278
  * every approval is recovered against that digest and the list is sorted by the
1273
1279
  * recovered owner address. Works for any owner key type (secp256k1, p256,
1274
- * webAuthn, keychain).
1280
+ * webAuthn).
1275
1281
  *
1276
- * Config updates never change `account`/`genesisConfigId`, so the genesis
1277
- * config is the correct input even for post-update transactions.
1282
+ * Config updates never change `account`, so the genesis config is the correct
1283
+ * input even for post-update transactions.
1278
1284
  *
1279
1285
  * @example
1280
1286
  * ```ts twoslash
@@ -1315,12 +1321,7 @@ export function sortMultisigApprovals(
1315
1321
  const digest = MultisigConfig.getSignPayload(
1316
1322
  'genesisConfig' in value && value.genesisConfig
1317
1323
  ? { payload, genesisConfig: value.genesisConfig }
1318
- : {
1319
- payload,
1320
- account: (value as { account: Address.Address }).account,
1321
- genesisConfigId: (value as { genesisConfigId: Hex.Hex })
1322
- .genesisConfigId,
1323
- },
1324
+ : { payload, account: (value as { account: Address.Address }).account },
1324
1325
  )
1325
1326
  // Recover each signer once (decorate–sort–undecorate) rather than inside the
1326
1327
  // comparator.
@@ -1337,20 +1338,17 @@ export declare namespace sortMultisigApprovals {
1337
1338
  type Value = {
1338
1339
  /** The inner transaction sign payload (`tx.signature_hash()`). */
1339
1340
  payload: Hex.Hex | Bytes.Bytes
1340
- /** The primitive owner approvals to order. */
1341
+ /** The owner approvals to order. */
1341
1342
  signatures: readonly SignatureEnvelope[]
1342
1343
  } & OneOf<
1343
1344
  | {
1344
1345
  /** The native multisig account address. */
1345
1346
  account: Address.Address
1346
- /** The permanent config ID. */
1347
- genesisConfigId: Hex.Hex
1348
1347
  }
1349
1348
  | {
1350
1349
  /**
1351
1350
  * The initial multisig config (the bootstrap config that derived the
1352
- * permanent `account` and `genesisConfigId`). Used to derive both values
1353
- * automatically.
1351
+ * permanent `account`). Used to derive the account automatically.
1354
1352
  */
1355
1353
  genesisConfig: MultisigConfig.Config
1356
1354
  }
@@ -1434,9 +1432,6 @@ export function toRpc(envelope: SignatureEnvelope): SignatureEnvelopeRpc {
1434
1432
  return {
1435
1433
  type: 'multisig',
1436
1434
  account: multisig.account,
1437
- // Map the typed envelope's `genesisConfigId` to the RPC wire field
1438
- // `configId` (TIP-1061 spec name).
1439
- configId: multisig.genesisConfigId,
1440
1435
  // Owner approvals are raw serialized signatures (node `Vec<Bytes>`).
1441
1436
  signatures: multisig.signatures.map((signature) => serialize(signature)),
1442
1437
  ...(multisig.init ? { init: multisig.init } : {}),
@@ -1706,6 +1701,16 @@ export class InvalidSerializedError extends Errors.BaseError {
1706
1701
  }
1707
1702
  }
1708
1703
 
1704
+ /**
1705
+ * Error thrown when a native multisig owner approval is invalid.
1706
+ */
1707
+ export class InvalidMultisigApprovalError extends Errors.BaseError {
1708
+ override readonly name = 'SignatureEnvelope.InvalidMultisigApprovalError'
1709
+ constructor({ reason }: { reason: string }) {
1710
+ super(`Invalid native multisig owner approval: ${reason}.`)
1711
+ }
1712
+ }
1713
+
1709
1714
  /**
1710
1715
  * Error thrown when a signature envelope fails to verify.
1711
1716
  */