ox 0.14.28 → 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
- configId: `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
- configId: 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
@@ -175,10 +177,9 @@ export type Multisig<bigintType = bigint, numberType = number> = {
175
177
  export type MultisigRpc = {
176
178
  type: 'multisig'
177
179
  account: Address.Address
178
- configId: Hex.Hex
179
180
  /**
180
- * Encoded primitive owner approvals (raw serialized signatures), matching the
181
- * node's `Vec<Bytes>` representation.
181
+ * Encoded owner approvals (raw serialized signatures), matching the node's
182
+ * `Vec<Bytes>` representation.
182
183
  */
183
184
  signatures: readonly Serialized[]
184
185
  init?: MultisigConfig.Config | undefined
@@ -328,11 +329,24 @@ export function assert(envelope: PartialBy<SignatureEnvelope, 'type'>): void {
328
329
  const multisig = envelope as Multisig
329
330
  const missing: string[] = []
330
331
  if (!multisig.account) missing.push('account')
331
- if (!multisig.configId) missing.push('configId')
332
332
  if (!Array.isArray(multisig.signatures)) missing.push('signatures')
333
333
  if (missing.length > 0)
334
334
  throw new MissingPropertiesError({ envelope, missing, type: 'multisig' })
335
- 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
+ }
336
350
  if (multisig.init) MultisigConfig.assert(multisig.init)
337
351
  return
338
352
  }
@@ -341,12 +355,22 @@ export function assert(envelope: PartialBy<SignatureEnvelope, 'type'>): void {
341
355
  export declare namespace assert {
342
356
  type ErrorType =
343
357
  | CoercionError
358
+ | InvalidMultisigApprovalError
344
359
  | MissingPropertiesError
345
360
  | MultisigConfig.assert.ErrorType
346
361
  | Signature.assert.ErrorType
347
362
  | Errors.GlobalErrorType
348
363
  }
349
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
+
350
374
  /**
351
375
  * Extracts the address of the signer from a {@link ox#SignatureEnvelope.SignatureEnvelope}.
352
376
  *
@@ -613,24 +637,26 @@ export function deserialize(value: Serialized): SignatureEnvelope {
613
637
  }
614
638
 
615
639
  if (typeId === serializedMultisigType) {
616
- // Wire format: `0x05 || rlp([account, configId, signatures, init])`. `init`
617
- // is optional: absent when the element is missing or the `0x80` placeholder
618
- // (decoded as the empty string `0x`), otherwise the bootstrap config list.
619
- const [account, configId, signatures, init] = Rlp.toHex(data) as [
620
- 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 [
621
644
  Hex.Hex,
622
645
  readonly Hex.Hex[],
623
646
  (Hex.Hex | MultisigConfig.Tuple)?,
624
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
+ })
625
654
  return {
626
655
  type: 'multisig',
627
656
  account,
628
- configId,
629
657
  signatures: signatures.map((signature) => deserialize(signature)),
630
- ...(init && init !== '0x'
631
- ? {
632
- init: MultisigConfig.fromTuple(init as MultisigConfig.Tuple),
633
- }
658
+ ...(init
659
+ ? { init: MultisigConfig.fromTuple(init as MultisigConfig.Tuple) }
634
660
  : {}),
635
661
  } satisfies Multisig
636
662
  }
@@ -753,6 +779,43 @@ export function deserialize(value: Serialized): SignatureEnvelope {
753
779
  * })
754
780
  * ```
755
781
  *
782
+ * @example
783
+ * ### Multisig (from genesis config)
784
+ *
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.
788
+ *
789
+ * ```ts twoslash
790
+ * import { Secp256k1 } from 'ox'
791
+ * import { MultisigConfig, SignatureEnvelope } from 'ox/tempo'
792
+ *
793
+ * const genesisConfig = MultisigConfig.from({
794
+ * threshold: 1,
795
+ * owners: [
796
+ * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
797
+ * ],
798
+ * })
799
+ *
800
+ * const privateKey = Secp256k1.randomPrivateKey()
801
+ * const signature = SignatureEnvelope.from(
802
+ * Secp256k1.sign({ payload: '0xdeadbeef', privateKey }),
803
+ * )
804
+ *
805
+ * // Bootstrap transaction
806
+ * const bootstrap = SignatureEnvelope.from({
807
+ * genesisConfig,
808
+ * signatures: [signature],
809
+ * init: true,
810
+ * })
811
+ *
812
+ * // Subsequent (non-bootstrap) transactions
813
+ * const subsequent = SignatureEnvelope.from({
814
+ * genesisConfig,
815
+ * signatures: [signature],
816
+ * })
817
+ * ```
818
+ *
756
819
  * @param value - The value to coerce (either a hex string or signature envelope).
757
820
  * @returns The signature envelope.
758
821
  */
@@ -774,20 +837,35 @@ export function from<const value extends from.Value>(
774
837
  const type = getType(value)
775
838
 
776
839
  if (type === 'multisig') {
777
- const multisig = value as Multisig
840
+ const multisig = value as Multisig & {
841
+ genesisConfig?: MultisigConfig.Config | undefined
842
+ init?: MultisigConfig.Config | boolean | undefined
843
+ }
844
+ const { genesisConfig, init, ...rest } = multisig
845
+ // Derive `account` from `genesisConfig` when not provided explicitly.
846
+ const account = (() => {
847
+ if (rest.account) return rest.account
848
+ if (genesisConfig) return MultisigConfig.getAddress(genesisConfig)
849
+ return rest.account
850
+ })()
851
+ // `init: true` opts into bootstrap using the supplied `genesisConfig`.
852
+ // Otherwise, `init` is treated as the explicit bootstrap config (or
853
+ // omitted).
854
+ const initSource = init === true ? genesisConfig : init || undefined
778
855
  return {
779
- ...multisig,
780
- signatures: multisig.signatures.map((signature) => from(signature)),
856
+ ...rest,
857
+ account,
858
+ signatures: rest.signatures.map((signature) => from(signature)),
781
859
  // Normalize the bootstrap config (sorts owners, defaults the salt) so the
782
860
  // in-memory envelope matches what `deserialize` reconstructs.
783
- ...(multisig.init ? { init: MultisigConfig.from(multisig.init) } : {}),
861
+ ...(initSource ? { init: MultisigConfig.from(initSource) } : {}),
784
862
  type,
785
863
  } as never
786
864
  }
787
865
 
788
866
  return {
789
867
  ...value,
790
- ...(type === 'p256' ? { prehash: value.prehash } : {}),
868
+ ...(type === 'p256' ? { prehash: (value as P256).prehash } : {}),
791
869
  ...(type === 'keychain'
792
870
  ? {
793
871
  ...(!(
@@ -827,10 +905,24 @@ export declare namespace from {
827
905
  payload?: Hex.Hex | Bytes.Bytes | undefined
828
906
  }
829
907
 
908
+ /**
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
912
+ * (non-bootstrap) transactions.
913
+ */
914
+ type MultisigFromGenesisConfig = {
915
+ type?: 'multisig' | undefined
916
+ genesisConfig: MultisigConfig.Config
917
+ signatures: readonly SignatureEnvelope[]
918
+ init?: MultisigConfig.Config | boolean | undefined
919
+ }
920
+
830
921
  type Value =
831
922
  | UnionPartialBy<SignatureEnvelope, 'prehash' | 'type'>
832
923
  | Secp256k1Flat
833
924
  | Serialized
925
+ | MultisigFromGenesisConfig
834
926
 
835
927
  type ReturnValue<value extends Value> = Compute<
836
928
  OneOf<
@@ -838,16 +930,18 @@ export declare namespace from {
838
930
  ? SignatureEnvelope
839
931
  : value extends Secp256k1Flat
840
932
  ? Secp256k1
841
- : IsNarrowable<value, SignatureEnvelope> extends true
842
- ? SignatureEnvelope
843
- : Assign<
844
- value,
845
- {
846
- readonly type: GetType<value>
847
- } & (GetType<value> extends 'keychain'
848
- ? { keyId?: Address.Address | undefined }
849
- : {})
850
- >
933
+ : value extends MultisigFromGenesisConfig
934
+ ? Multisig
935
+ : IsNarrowable<value, SignatureEnvelope> extends true
936
+ ? SignatureEnvelope
937
+ : Assign<
938
+ value,
939
+ {
940
+ readonly type: GetType<value>
941
+ } & (GetType<value> extends 'keychain'
942
+ ? { keyId?: Address.Address | undefined }
943
+ : {})
944
+ >
851
945
  >
852
946
  >
853
947
  }
@@ -955,15 +1049,12 @@ export function fromRpc(envelope: SignatureEnvelopeRpc): SignatureEnvelope {
955
1049
 
956
1050
  if (
957
1051
  envelope.type === 'multisig' ||
958
- ('account' in envelope &&
959
- 'configId' in envelope &&
960
- 'signatures' in envelope)
1052
+ ('account' in envelope && 'signatures' in envelope)
961
1053
  ) {
962
1054
  const multisig = envelope as MultisigRpc
963
1055
  return {
964
1056
  type: 'multisig',
965
1057
  account: multisig.account,
966
- configId: multisig.configId,
967
1058
  // Owner approvals are raw serialized signatures (node `Vec<Bytes>`).
968
1059
  signatures: multisig.signatures.map((signature) =>
969
1060
  deserialize(signature),
@@ -1050,8 +1141,7 @@ export function getType<
1050
1141
 
1051
1142
  // Detect Multisig signature
1052
1143
  if (
1053
- 'account' in envelope &&
1054
- 'configId' in envelope &&
1144
+ ('account' in envelope || 'genesisConfig' in envelope) &&
1055
1145
  'signatures' in envelope
1056
1146
  )
1057
1147
  return 'multisig' as never
@@ -1151,17 +1241,15 @@ export function serialize(
1151
1241
 
1152
1242
  if (type === 'multisig') {
1153
1243
  const multisig = envelope as Multisig
1154
- // Format: `0x05 || rlp([account, configId, signatures, init])`, where each
1155
- // owner approval is an encoded primitive signature. `init` is the bootstrap
1156
- // config (an RLP list) when present, otherwise the canonical empty-string
1157
- // 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.
1158
1247
  return Hex.concat(
1159
1248
  serializedMultisigType,
1160
1249
  Rlp.fromHex([
1161
1250
  multisig.account,
1162
- multisig.configId,
1163
1251
  multisig.signatures.map((signature) => serialize(signature)),
1164
- multisig.init ? MultisigConfig.toTuple(multisig.init) : '0x',
1252
+ ...(multisig.init ? [MultisigConfig.toTuple(multisig.init)] : []),
1165
1253
  ]),
1166
1254
  options.magic ? magicBytes : '0x',
1167
1255
  )
@@ -1189,35 +1277,35 @@ export declare namespace serialize {
1189
1277
  * ({@link ox#MultisigConfig.(getSignPayload:function)}), so the signer of
1190
1278
  * every approval is recovered against that digest and the list is sorted by the
1191
1279
  * recovered owner address. Works for any owner key type (secp256k1, p256,
1192
- * webAuthn, keychain).
1280
+ * webAuthn).
1281
+ *
1282
+ * Config updates never change `account`, so the genesis config is the correct
1283
+ * input even for post-update transactions.
1193
1284
  *
1194
1285
  * @example
1195
1286
  * ```ts twoslash
1196
1287
  * import { Secp256k1 } from 'ox'
1197
1288
  * import { MultisigConfig, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo'
1198
1289
  *
1199
- * const config = MultisigConfig.from({
1290
+ * const genesisConfig = MultisigConfig.from({
1200
1291
  * threshold: 2,
1201
1292
  * owners: [
1202
1293
  * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
1203
1294
  * { owner: '0x2222222222222222222222222222222222222222', weight: 1 },
1204
1295
  * ],
1205
1296
  * })
1206
- * const configId = MultisigConfig.toId(config)
1207
- * const account = MultisigConfig.getAddress({ configId })
1208
1297
  *
1209
1298
  * const tx = TxEnvelopeTempo.from({ chainId: 1, calls: [] })
1210
1299
  * const payload = TxEnvelopeTempo.getSignPayload(tx)
1211
- * const digest = MultisigConfig.getSignPayload({ payload, account, configId })
1212
1300
  *
1213
1301
  * const privateKeys = [Secp256k1.randomPrivateKey(), Secp256k1.randomPrivateKey()]
1302
+ * const digest = MultisigConfig.getSignPayload({ payload, genesisConfig })
1214
1303
  * const signatures = privateKeys.map((privateKey) =>
1215
1304
  * SignatureEnvelope.from(Secp256k1.sign({ payload: digest, privateKey })),
1216
1305
  * )
1217
1306
  *
1218
1307
  * const ordered = SignatureEnvelope.sortMultisigApprovals({ // [!code focus]
1219
- * account, // [!code focus]
1220
- * configId, // [!code focus]
1308
+ * genesisConfig, // [!code focus]
1221
1309
  * payload, // [!code focus]
1222
1310
  * signatures, // [!code focus]
1223
1311
  * }) // [!code focus]
@@ -1229,12 +1317,12 @@ export declare namespace serialize {
1229
1317
  export function sortMultisigApprovals(
1230
1318
  value: sortMultisigApprovals.Value,
1231
1319
  ): readonly SignatureEnvelope[] {
1232
- const { account, configId, payload, signatures } = value
1233
- const digest = MultisigConfig.getSignPayload({
1234
- account,
1235
- configId,
1236
- payload,
1237
- })
1320
+ const { payload, signatures } = value
1321
+ const digest = MultisigConfig.getSignPayload(
1322
+ 'genesisConfig' in value && value.genesisConfig
1323
+ ? { payload, genesisConfig: value.genesisConfig }
1324
+ : { payload, account: (value as { account: Address.Address }).account },
1325
+ )
1238
1326
  // Recover each signer once (decorate–sort–undecorate) rather than inside the
1239
1327
  // comparator.
1240
1328
  return signatures
@@ -1248,15 +1336,23 @@ export function sortMultisigApprovals(
1248
1336
 
1249
1337
  export declare namespace sortMultisigApprovals {
1250
1338
  type Value = {
1251
- /** The native multisig account address. */
1252
- account: Address.Address
1253
- /** The permanent config ID. */
1254
- configId: Hex.Hex
1255
1339
  /** The inner transaction sign payload (`tx.signature_hash()`). */
1256
1340
  payload: Hex.Hex | Bytes.Bytes
1257
- /** The primitive owner approvals to order. */
1341
+ /** The owner approvals to order. */
1258
1342
  signatures: readonly SignatureEnvelope[]
1259
- }
1343
+ } & OneOf<
1344
+ | {
1345
+ /** The native multisig account address. */
1346
+ account: Address.Address
1347
+ }
1348
+ | {
1349
+ /**
1350
+ * The initial multisig config (the bootstrap config that derived the
1351
+ * permanent `account`). Used to derive the account automatically.
1352
+ */
1353
+ genesisConfig: MultisigConfig.Config
1354
+ }
1355
+ >
1260
1356
 
1261
1357
  type ErrorType =
1262
1358
  | MultisigConfig.getSignPayload.ErrorType
@@ -1336,7 +1432,6 @@ export function toRpc(envelope: SignatureEnvelope): SignatureEnvelopeRpc {
1336
1432
  return {
1337
1433
  type: 'multisig',
1338
1434
  account: multisig.account,
1339
- configId: multisig.configId,
1340
1435
  // Owner approvals are raw serialized signatures (node `Vec<Bytes>`).
1341
1436
  signatures: multisig.signatures.map((signature) => serialize(signature)),
1342
1437
  ...(multisig.init ? { init: multisig.init } : {}),
@@ -1606,6 +1701,16 @@ export class InvalidSerializedError extends Errors.BaseError {
1606
1701
  }
1607
1702
  }
1608
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
+
1609
1714
  /**
1610
1715
  * Error thrown when a signature envelope fails to verify.
1611
1716
  */